1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "session.h"
27
28 #include "buildconfiguration.h"
29 #include "deployconfiguration.h"
30 #include "editorconfiguration.h"
31 #include "foldernavigationwidget.h"
32 #include "kit.h"
33 #include "project.h"
34 #include "projectexplorer.h"
35 #include "projectnodes.h"
36 #include "target.h"
37
38 #include <coreplugin/coreconstants.h>
39 #include <coreplugin/editormanager/editormanager.h>
40 #include <coreplugin/icore.h>
41 #include <coreplugin/idocument.h>
42 #include <coreplugin/imode.h>
43 #include <coreplugin/modemanager.h>
44 #include <coreplugin/progressmanager/progressmanager.h>
45
46 #include <texteditor/texteditor.h>
47
48 #include <utils/algorithm.h>
49 #include <utils/stylehelper.h>
50
51 #include <QDebug>
52 #include <QDir>
53 #include <QFileInfo>
54
55 #include <QMessageBox>
56 #include <QPushButton>
57
58 #ifdef WITH_TESTS
59 #include <QTemporaryFile>
60 #include <QTest>
61 #include <vector>
62 #endif
63
64 using namespace Core;
65 using namespace Utils;
66 using namespace ProjectExplorer::Internal;
67
68 namespace ProjectExplorer {
69
70 /*!
71 \class ProjectExplorer::SessionManager
72
73 \brief The SessionManager class manages sessions.
74
75 TODO the interface of this class is not really great.
76 The implementation suffers from that all the functions from the
77 public interface just wrap around functions which do the actual work.
78 This could be improved.
79 */
80
81 class SessionManagerPrivate
82 {
83 public:
84 void restoreValues(const PersistentSettingsReader &reader);
85 void restoreDependencies(const PersistentSettingsReader &reader);
86 void restoreStartupProject(const PersistentSettingsReader &reader);
87 void restoreEditors(const PersistentSettingsReader &reader);
88 void restoreProjects(const QStringList &fileList);
89 void askUserAboutFailedProjects();
90 void sessionLoadingProgress();
91
92 bool recursiveDependencyCheck(const QString &newDep, const QString &checkDep) const;
93 QStringList dependencies(const QString &proName) const;
94 QStringList dependenciesOrder() const;
95 void dependencies(const QString &proName, QStringList &result) const;
96
97 static QString windowTitleAddition(const QString &filePath);
98 static QString sessionTitle(const QString &filePath);
99
hasProjects() const100 bool hasProjects() const { return !m_projects.isEmpty(); }
101
102 QString m_sessionName = QLatin1String("default");
103 bool m_virginSession = true;
104 bool m_loadingSession = false;
105 bool m_casadeSetActive = false;
106
107 mutable QStringList m_sessions;
108 mutable QHash<QString, QDateTime> m_sessionDateTimes;
109
110 Project *m_startupProject = nullptr;
111 QList<Project *> m_projects;
112 QStringList m_failedProjects;
113 QMap<QString, QStringList> m_depMap;
114 QMap<QString, QVariant> m_values;
115 QFutureInterface<void> m_future;
116 PersistentSettingsWriter *m_writer = nullptr;
117
118 private:
119 static QString locationInProject(const QString &filePath);
120 };
121
122 static SessionManager *m_instance = nullptr;
123 static SessionManagerPrivate *d = nullptr;
124
projectFolderId(Project * pro)125 static QString projectFolderId(Project *pro)
126 {
127 return pro->projectFilePath().toString();
128 }
129
130 const int PROJECT_SORT_VALUE = 100;
131
SessionManager(QObject * parent)132 SessionManager::SessionManager(QObject *parent) : QObject(parent)
133 {
134 m_instance = this;
135 d = new SessionManagerPrivate;
136
137 connect(ModeManager::instance(), &ModeManager::currentModeChanged,
138 this, &SessionManager::saveActiveMode);
139
140 connect(EditorManager::instance(), &EditorManager::editorCreated,
141 this, &SessionManager::configureEditor);
142 connect(this, &SessionManager::projectAdded,
143 EditorManager::instance(), &EditorManager::updateWindowTitles);
144 connect(this, &SessionManager::projectRemoved,
145 EditorManager::instance(), &EditorManager::updateWindowTitles);
146 connect(this, &SessionManager::projectDisplayNameChanged,
147 EditorManager::instance(), &EditorManager::updateWindowTitles);
148 connect(EditorManager::instance(), &EditorManager::editorOpened,
149 this, &SessionManager::markSessionFileDirty);
150 connect(EditorManager::instance(), &EditorManager::editorsClosed,
151 this, &SessionManager::markSessionFileDirty);
152
153 EditorManager::setWindowTitleAdditionHandler(&SessionManagerPrivate::windowTitleAddition);
154 EditorManager::setSessionTitleHandler(&SessionManagerPrivate::sessionTitle);
155 }
156
~SessionManager()157 SessionManager::~SessionManager()
158 {
159 EditorManager::setWindowTitleAdditionHandler({});
160 EditorManager::setSessionTitleHandler({});
161 emit m_instance->aboutToUnloadSession(d->m_sessionName);
162 delete d->m_writer;
163 delete d;
164 d = nullptr;
165 }
166
instance()167 SessionManager *SessionManager::instance()
168 {
169 return m_instance;
170 }
171
isDefaultVirgin()172 bool SessionManager::isDefaultVirgin()
173 {
174 return isDefaultSession(d->m_sessionName) && d->m_virginSession;
175 }
176
isDefaultSession(const QString & session)177 bool SessionManager::isDefaultSession(const QString &session)
178 {
179 return session == QLatin1String("default");
180 }
181
saveActiveMode(Id mode)182 void SessionManager::saveActiveMode(Id mode)
183 {
184 if (mode != Core::Constants::MODE_WELCOME)
185 setValue(QLatin1String("ActiveMode"), mode.toString());
186 }
187
recursiveDependencyCheck(const QString & newDep,const QString & checkDep) const188 bool SessionManagerPrivate::recursiveDependencyCheck(const QString &newDep, const QString &checkDep) const
189 {
190 if (newDep == checkDep)
191 return false;
192
193 foreach (const QString &dependency, m_depMap.value(checkDep)) {
194 if (!recursiveDependencyCheck(newDep, dependency))
195 return false;
196 }
197
198 return true;
199 }
200
201 /*
202 * The dependency management exposes an interface based on projects, but
203 * is internally purely string based. This is suboptimal. Probably it would be
204 * nicer to map the filenames to projects on load and only map it back to
205 * filenames when saving.
206 */
207
dependencies(const Project * project)208 QList<Project *> SessionManager::dependencies(const Project *project)
209 {
210 const QString proName = project->projectFilePath().toString();
211 const QStringList proDeps = d->m_depMap.value(proName);
212
213 QList<Project *> projects;
214 foreach (const QString &dep, proDeps) {
215 const Utils::FilePath fn = Utils::FilePath::fromString(dep);
216 Project *pro = Utils::findOrDefault(d->m_projects, [&fn](Project *p) { return p->projectFilePath() == fn; });
217 if (pro)
218 projects += pro;
219 }
220
221 return projects;
222 }
223
hasDependency(const Project * project,const Project * depProject)224 bool SessionManager::hasDependency(const Project *project, const Project *depProject)
225 {
226 const QString proName = project->projectFilePath().toString();
227 const QString depName = depProject->projectFilePath().toString();
228
229 const QStringList proDeps = d->m_depMap.value(proName);
230 return proDeps.contains(depName);
231 }
232
canAddDependency(const Project * project,const Project * depProject)233 bool SessionManager::canAddDependency(const Project *project, const Project *depProject)
234 {
235 const QString newDep = project->projectFilePath().toString();
236 const QString checkDep = depProject->projectFilePath().toString();
237
238 return d->recursiveDependencyCheck(newDep, checkDep);
239 }
240
addDependency(Project * project,Project * depProject)241 bool SessionManager::addDependency(Project *project, Project *depProject)
242 {
243 const QString proName = project->projectFilePath().toString();
244 const QString depName = depProject->projectFilePath().toString();
245
246 // check if this dependency is valid
247 if (!d->recursiveDependencyCheck(proName, depName))
248 return false;
249
250 QStringList proDeps = d->m_depMap.value(proName);
251 if (!proDeps.contains(depName)) {
252 proDeps.append(depName);
253 d->m_depMap[proName] = proDeps;
254 }
255 emit m_instance->dependencyChanged(project, depProject);
256
257 return true;
258 }
259
removeDependency(Project * project,Project * depProject)260 void SessionManager::removeDependency(Project *project, Project *depProject)
261 {
262 const QString proName = project->projectFilePath().toString();
263 const QString depName = depProject->projectFilePath().toString();
264
265 QStringList proDeps = d->m_depMap.value(proName);
266 proDeps.removeAll(depName);
267 if (proDeps.isEmpty())
268 d->m_depMap.remove(proName);
269 else
270 d->m_depMap[proName] = proDeps;
271 emit m_instance->dependencyChanged(project, depProject);
272 }
273
isProjectConfigurationCascading()274 bool SessionManager::isProjectConfigurationCascading()
275 {
276 return d->m_casadeSetActive;
277 }
278
setProjectConfigurationCascading(bool b)279 void SessionManager::setProjectConfigurationCascading(bool b)
280 {
281 d->m_casadeSetActive = b;
282 markSessionFileDirty();
283 }
284
setActiveTarget(Project * project,Target * target,SetActive cascade)285 void SessionManager::setActiveTarget(Project *project, Target *target, SetActive cascade)
286 {
287 QTC_ASSERT(project, return);
288
289 if (project->isShuttingDown())
290 return;
291
292 project->setActiveTarget(target);
293
294 if (!target) // never cascade setting no target
295 return;
296
297 if (cascade != SetActive::Cascade || !d->m_casadeSetActive)
298 return;
299
300 Utils::Id kitId = target->kit()->id();
301 for (Project *otherProject : SessionManager::projects()) {
302 if (otherProject == project)
303 continue;
304 if (Target *otherTarget = Utils::findOrDefault(otherProject->targets(),
305 [kitId](Target *t) { return t->kit()->id() == kitId; }))
306 otherProject->setActiveTarget(otherTarget);
307 }
308 }
309
setActiveBuildConfiguration(Target * target,BuildConfiguration * bc,SetActive cascade)310 void SessionManager::setActiveBuildConfiguration(Target *target, BuildConfiguration *bc, SetActive cascade)
311 {
312 QTC_ASSERT(target, return);
313 QTC_ASSERT(target->project(), return);
314
315 if (target->project()->isShuttingDown() || target->isShuttingDown())
316 return;
317
318 target->setActiveBuildConfiguration(bc);
319
320 if (!bc)
321 return;
322 if (cascade != SetActive::Cascade || !d->m_casadeSetActive)
323 return;
324
325 Utils::Id kitId = target->kit()->id();
326 QString name = bc->displayName(); // We match on displayname
327 for (Project *otherProject : SessionManager::projects()) {
328 if (otherProject == target->project())
329 continue;
330 Target *otherTarget = otherProject->activeTarget();
331 if (!otherTarget || otherTarget->kit()->id() != kitId)
332 continue;
333
334 for (BuildConfiguration *otherBc : otherTarget->buildConfigurations()) {
335 if (otherBc->displayName() == name) {
336 otherTarget->setActiveBuildConfiguration(otherBc);
337 break;
338 }
339 }
340 }
341 }
342
setActiveDeployConfiguration(Target * target,DeployConfiguration * dc,SetActive cascade)343 void SessionManager::setActiveDeployConfiguration(Target *target, DeployConfiguration *dc, SetActive cascade)
344 {
345 QTC_ASSERT(target, return);
346 QTC_ASSERT(target->project(), return);
347
348 if (target->project()->isShuttingDown() || target->isShuttingDown())
349 return;
350
351 target->setActiveDeployConfiguration(dc);
352
353 if (!dc)
354 return;
355 if (cascade != SetActive::Cascade || !d->m_casadeSetActive)
356 return;
357
358 Utils::Id kitId = target->kit()->id();
359 QString name = dc->displayName(); // We match on displayname
360 for (Project *otherProject : SessionManager::projects()) {
361 if (otherProject == target->project())
362 continue;
363 Target *otherTarget = otherProject->activeTarget();
364 if (!otherTarget || otherTarget->kit()->id() != kitId)
365 continue;
366
367 for (DeployConfiguration *otherDc : otherTarget->deployConfigurations()) {
368 if (otherDc->displayName() == name) {
369 otherTarget->setActiveDeployConfiguration(otherDc);
370 break;
371 }
372 }
373 }
374 }
375
setStartupProject(Project * startupProject)376 void SessionManager::setStartupProject(Project *startupProject)
377 {
378 QTC_ASSERT((!startupProject && d->m_projects.isEmpty())
379 || (startupProject && d->m_projects.contains(startupProject)), return);
380
381 if (d->m_startupProject == startupProject)
382 return;
383
384 d->m_startupProject = startupProject;
385 if (d->m_startupProject && d->m_startupProject->needsConfiguration()) {
386 ModeManager::activateMode(Constants::MODE_SESSION);
387 ModeManager::setFocusToCurrentMode();
388 }
389 emit m_instance->startupProjectChanged(startupProject);
390 }
391
startupProject()392 Project *SessionManager::startupProject()
393 {
394 return d->m_startupProject;
395 }
396
startupTarget()397 Target *SessionManager::startupTarget()
398 {
399 return d->m_startupProject ? d->m_startupProject->activeTarget() : nullptr;
400 }
401
startupBuildSystem()402 BuildSystem *SessionManager::startupBuildSystem()
403 {
404 Target *t = startupTarget();
405 return t ? t->buildSystem() : nullptr;
406 }
407
408 /*!
409 * Returns the RunConfiguration of the currently active target
410 * of the startup project, if such exists, or \c nullptr otherwise.
411 */
412
413
startupRunConfiguration()414 RunConfiguration *SessionManager::startupRunConfiguration()
415 {
416 Target *t = startupTarget();
417 return t ? t->activeRunConfiguration() : nullptr;
418 }
419
addProject(Project * pro)420 void SessionManager::addProject(Project *pro)
421 {
422 QTC_ASSERT(pro, return);
423 QTC_CHECK(!pro->displayName().isEmpty());
424 QTC_CHECK(pro->id().isValid());
425
426 d->m_virginSession = false;
427 QTC_ASSERT(!d->m_projects.contains(pro), return);
428
429 d->m_projects.append(pro);
430
431 connect(pro, &Project::displayNameChanged,
432 m_instance, [pro]() { emit m_instance->projectDisplayNameChanged(pro); });
433
434 emit m_instance->projectAdded(pro);
435 const auto updateFolderNavigation = [pro] {
436 // destructing projects might trigger changes, so check if the project is actually there
437 if (QTC_GUARD(d->m_projects.contains(pro))) {
438 const QIcon icon = pro->rootProjectNode() ? pro->rootProjectNode()->icon() : QIcon();
439 FolderNavigationWidgetFactory::insertRootDirectory({projectFolderId(pro),
440 PROJECT_SORT_VALUE,
441 pro->displayName(),
442 pro->projectFilePath().parentDir(),
443 icon});
444 }
445 };
446 updateFolderNavigation();
447 configureEditors(pro);
448 connect(pro, &Project::fileListChanged, m_instance, [pro, updateFolderNavigation]() {
449 configureEditors(pro);
450 updateFolderNavigation(); // update icon
451 });
452 connect(pro, &Project::displayNameChanged, m_instance, updateFolderNavigation);
453
454 if (!startupProject())
455 setStartupProject(pro);
456 }
457
removeProject(Project * project)458 void SessionManager::removeProject(Project *project)
459 {
460 d->m_virginSession = false;
461 QTC_ASSERT(project, return);
462 removeProjects({project});
463 }
464
loadingSession()465 bool SessionManager::loadingSession()
466 {
467 return d->m_loadingSession;
468 }
469
save()470 bool SessionManager::save()
471 {
472 emit m_instance->aboutToSaveSession();
473
474 const FilePath filePath = sessionNameToFileName(d->m_sessionName);
475 QVariantMap data;
476
477 // See the explanation at loadSession() for how we handle the implicit default session.
478 if (isDefaultVirgin()) {
479 if (filePath.exists()) {
480 PersistentSettingsReader reader;
481 if (!reader.load(filePath)) {
482 QMessageBox::warning(ICore::dialogParent(), tr("Error while saving session"),
483 tr("Could not save session %1").arg(filePath.toUserOutput()));
484 return false;
485 }
486 data = reader.restoreValues();
487 }
488 } else {
489 // save the startup project
490 if (d->m_startupProject) {
491 data.insert(QLatin1String("StartupProject"),
492 d->m_startupProject->projectFilePath().toString());
493 }
494
495 const QColor c = StyleHelper::requestedBaseColor();
496 if (c.isValid()) {
497 QString tmp = QString::fromLatin1("#%1%2%3")
498 .arg(c.red(), 2, 16, QLatin1Char('0'))
499 .arg(c.green(), 2, 16, QLatin1Char('0'))
500 .arg(c.blue(), 2, 16, QLatin1Char('0'));
501 data.insert(QLatin1String("Color"), tmp);
502 }
503
504 QStringList projectFiles = Utils::transform(projects(), [](Project *p) {
505 return p->projectFilePath().toString();
506 });
507 // Restore information on projects that failed to load:
508 // don't read projects to the list, which the user loaded
509 foreach (const QString &failed, d->m_failedProjects) {
510 if (!projectFiles.contains(failed))
511 projectFiles << failed;
512 }
513
514 data.insert(QLatin1String("ProjectList"), projectFiles);
515 data.insert(QLatin1String("CascadeSetActive"), d->m_casadeSetActive);
516
517 QVariantMap depMap;
518 auto i = d->m_depMap.constBegin();
519 while (i != d->m_depMap.constEnd()) {
520 QString key = i.key();
521 QStringList values;
522 foreach (const QString &value, i.value())
523 values << value;
524 depMap.insert(key, values);
525 ++i;
526 }
527 data.insert(QLatin1String("ProjectDependencies"), QVariant(depMap));
528 data.insert(QLatin1String("EditorSettings"), EditorManager::saveState().toBase64());
529 }
530
531 const auto end = d->m_values.constEnd();
532 QStringList keys;
533 for (auto it = d->m_values.constBegin(); it != end; ++it) {
534 data.insert(QLatin1String("value-") + it.key(), it.value());
535 keys << it.key();
536 }
537 data.insert(QLatin1String("valueKeys"), keys);
538
539 if (!d->m_writer || d->m_writer->fileName() != filePath) {
540 delete d->m_writer;
541 d->m_writer = new PersistentSettingsWriter(filePath, "QtCreatorSession");
542 }
543 const bool result = d->m_writer->save(data, ICore::dialogParent());
544 if (result) {
545 if (!isDefaultVirgin())
546 d->m_sessionDateTimes.insert(activeSession(), QDateTime::currentDateTime());
547 } else {
548 QMessageBox::warning(ICore::dialogParent(), tr("Error while saving session"),
549 tr("Could not save session to file %1").arg(d->m_writer->fileName().toUserOutput()));
550 }
551
552 return result;
553 }
554
555 /*!
556 Closes all projects
557 */
closeAllProjects()558 void SessionManager::closeAllProjects()
559 {
560 removeProjects(projects());
561 }
562
projects()563 const QList<Project *> SessionManager::projects()
564 {
565 return d->m_projects;
566 }
567
hasProjects()568 bool SessionManager::hasProjects()
569 {
570 return d->hasProjects();
571 }
572
hasProject(Project * p)573 bool SessionManager::hasProject(Project *p)
574 {
575 return d->m_projects.contains(p);
576 }
577
dependencies(const QString & proName) const578 QStringList SessionManagerPrivate::dependencies(const QString &proName) const
579 {
580 QStringList result;
581 dependencies(proName, result);
582 return result;
583 }
584
dependencies(const QString & proName,QStringList & result) const585 void SessionManagerPrivate::dependencies(const QString &proName, QStringList &result) const
586 {
587 QStringList depends = m_depMap.value(proName);
588
589 foreach (const QString &dep, depends)
590 dependencies(dep, result);
591
592 if (!result.contains(proName))
593 result.append(proName);
594 }
595
sessionTitle(const QString & filePath)596 QString SessionManagerPrivate::sessionTitle(const QString &filePath)
597 {
598 if (SessionManager::isDefaultSession(d->m_sessionName)) {
599 if (filePath.isEmpty()) {
600 // use single project's name if there is only one loaded.
601 const QList<Project *> projects = SessionManager::projects();
602 if (projects.size() == 1)
603 return projects.first()->displayName();
604 }
605 } else {
606 QString sessionName = d->m_sessionName;
607 if (sessionName.isEmpty())
608 sessionName = SessionManager::tr("Untitled");
609 return sessionName;
610 }
611 return QString();
612 }
613
locationInProject(const QString & filePath)614 QString SessionManagerPrivate::locationInProject(const QString &filePath) {
615 const Project *project = SessionManager::projectForFile(Utils::FilePath::fromString(filePath));
616 if (!project)
617 return QString();
618
619 const Utils::FilePath file = Utils::FilePath::fromString(filePath);
620 const Utils::FilePath parentDir = file.parentDir();
621 if (parentDir == project->projectDirectory())
622 return "@ " + project->displayName();
623
624 if (file.isChildOf(project->projectDirectory())) {
625 const Utils::FilePath dirInProject = parentDir.relativeChildPath(project->projectDirectory());
626 return "(" + dirInProject.toUserOutput() + " @ " + project->displayName() + ")";
627 }
628
629 // For a file that is "outside" the project it belongs to, we display its
630 // dir's full path because it is easier to read than a series of "../../.".
631 // Example: /home/hugo/GenericProject/App.files lists /home/hugo/lib/Bar.cpp
632 return "(" + parentDir.toUserOutput() + " @ " + project->displayName() + ")";
633 }
634
windowTitleAddition(const QString & filePath)635 QString SessionManagerPrivate::windowTitleAddition(const QString &filePath)
636 {
637 return locationInProject(filePath);
638 }
639
dependenciesOrder() const640 QStringList SessionManagerPrivate::dependenciesOrder() const
641 {
642 QList<QPair<QString, QStringList> > unordered;
643 QStringList ordered;
644
645 // copy the map to a temporary list
646 for (const Project *pro : m_projects) {
647 const QString proName = pro->projectFilePath().toString();
648 const QStringList depList = filtered(m_depMap.value(proName),
649 [this](const QString &proPath) {
650 return contains(m_projects, [proPath](const Project *p) {
651 return p->projectFilePath().toString() == proPath;
652 });
653 });
654 unordered << qMakePair(proName, depList);
655 }
656
657 while (!unordered.isEmpty()) {
658 for (int i = (unordered.count() - 1); i >= 0; --i) {
659 if (unordered.at(i).second.isEmpty()) {
660 ordered << unordered.at(i).first;
661 unordered.removeAt(i);
662 }
663 }
664
665 // remove the handled projects from the dependency lists
666 // of the remaining unordered projects
667 for (int i = 0; i < unordered.count(); ++i) {
668 foreach (const QString &pro, ordered) {
669 QStringList depList = unordered.at(i).second;
670 depList.removeAll(pro);
671 unordered[i].second = depList;
672 }
673 }
674 }
675
676 return ordered;
677 }
678
projectOrder(const Project * project)679 QList<Project *> SessionManager::projectOrder(const Project *project)
680 {
681 QList<Project *> result;
682
683 QStringList pros;
684 if (project)
685 pros = d->dependencies(project->projectFilePath().toString());
686 else
687 pros = d->dependenciesOrder();
688
689 foreach (const QString &proFile, pros) {
690 for (Project *pro : projects()) {
691 if (pro->projectFilePath().toString() == proFile) {
692 result << pro;
693 break;
694 }
695 }
696 }
697
698 return result;
699 }
700
projectForFile(const Utils::FilePath & fileName)701 Project *SessionManager::projectForFile(const Utils::FilePath &fileName)
702 {
703 return Utils::findOrDefault(SessionManager::projects(),
704 [&fileName](const Project *p) { return p->isKnownFile(fileName); });
705 }
706
configureEditor(IEditor * editor,const QString & fileName)707 void SessionManager::configureEditor(IEditor *editor, const QString &fileName)
708 {
709 if (auto textEditor = qobject_cast<TextEditor::BaseTextEditor*>(editor)) {
710 Project *project = projectForFile(Utils::FilePath::fromString(fileName));
711 // Global settings are the default.
712 if (project)
713 project->editorConfiguration()->configureEditor(textEditor);
714 }
715 }
716
configureEditors(Project * project)717 void SessionManager::configureEditors(Project *project)
718 {
719 foreach (IDocument *document, DocumentModel::openedDocuments()) {
720 if (project->isKnownFile(document->filePath())) {
721 foreach (IEditor *editor, DocumentModel::editorsForDocument(document)) {
722 if (auto textEditor = qobject_cast<TextEditor::BaseTextEditor*>(editor)) {
723 project->editorConfiguration()->configureEditor(textEditor);
724 }
725 }
726 }
727 }
728 }
729
removeProjects(const QList<Project * > & remove)730 void SessionManager::removeProjects(const QList<Project *> &remove)
731 {
732 for (Project *pro : remove)
733 emit m_instance->aboutToRemoveProject(pro);
734
735 bool changeStartupProject = false;
736
737 // Delete projects
738 for (Project *pro : remove) {
739 pro->saveSettings();
740 pro->markAsShuttingDown();
741
742 // Remove the project node:
743 d->m_projects.removeOne(pro);
744
745 if (pro == d->m_startupProject)
746 changeStartupProject = true;
747
748 FolderNavigationWidgetFactory::removeRootDirectory(projectFolderId(pro));
749 disconnect(pro, nullptr, m_instance, nullptr);
750 emit m_instance->projectRemoved(pro);
751 }
752
753 if (changeStartupProject)
754 setStartupProject(hasProjects() ? projects().first() : nullptr);
755
756 qDeleteAll(remove);
757 }
758
759 /*!
760 Lets other plugins store persistent values within the session file.
761 */
762
setValue(const QString & name,const QVariant & value)763 void SessionManager::setValue(const QString &name, const QVariant &value)
764 {
765 if (d->m_values.value(name) == value)
766 return;
767 d->m_values.insert(name, value);
768 }
769
value(const QString & name)770 QVariant SessionManager::value(const QString &name)
771 {
772 auto it = d->m_values.constFind(name);
773 return (it == d->m_values.constEnd()) ? QVariant() : *it;
774 }
775
activeSession()776 QString SessionManager::activeSession()
777 {
778 return d->m_sessionName;
779 }
780
sessions()781 QStringList SessionManager::sessions()
782 {
783 if (d->m_sessions.isEmpty()) {
784 // We are not initialized yet, so do that now
785 const FilePaths sessionFiles =
786 ICore::userResourcePath().dirEntries({"*.qws"}, QDir::NoFilter, QDir::Time);
787 for (const FilePath &file : sessionFiles) {
788 const QString &name = file.completeBaseName();
789 d->m_sessionDateTimes.insert(name, file.lastModified());
790 if (name != QLatin1String("default"))
791 d->m_sessions << name;
792 }
793 d->m_sessions.prepend(QLatin1String("default"));
794 }
795 return d->m_sessions;
796 }
797
sessionDateTime(const QString & session)798 QDateTime SessionManager::sessionDateTime(const QString &session)
799 {
800 return d->m_sessionDateTimes.value(session);
801 }
802
sessionNameToFileName(const QString & session)803 FilePath SessionManager::sessionNameToFileName(const QString &session)
804 {
805 return ICore::userResourcePath(session + ".qws");
806 }
807
808 /*!
809 Creates \a session, but does not actually create the file.
810 */
811
createSession(const QString & session)812 bool SessionManager::createSession(const QString &session)
813 {
814 if (sessions().contains(session))
815 return false;
816 Q_ASSERT(d->m_sessions.size() > 0);
817 d->m_sessions.insert(1, session);
818 return true;
819 }
820
renameSession(const QString & original,const QString & newName)821 bool SessionManager::renameSession(const QString &original, const QString &newName)
822 {
823 if (!cloneSession(original, newName))
824 return false;
825 if (original == activeSession())
826 loadSession(newName);
827 return deleteSession(original);
828 }
829
830
831 /*!
832 \brief Shows a dialog asking the user to confirm deleting the session \p session
833 */
confirmSessionDelete(const QStringList & sessions)834 bool SessionManager::confirmSessionDelete(const QStringList &sessions)
835 {
836 const QString title = sessions.size() == 1 ? tr("Delete Session") : tr("Delete Sessions");
837 const QString question = sessions.size() == 1
838 ? tr("Delete session %1?").arg(sessions.first())
839 : tr("Delete these sessions?\n %1").arg(sessions.join("\n "));
840 return QMessageBox::question(ICore::dialogParent(),
841 title,
842 question,
843 QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes;
844 }
845
846 /*!
847 Deletes \a session name from session list and the file from disk.
848 */
deleteSession(const QString & session)849 bool SessionManager::deleteSession(const QString &session)
850 {
851 if (!d->m_sessions.contains(session))
852 return false;
853 d->m_sessions.removeOne(session);
854 QFile fi(sessionNameToFileName(session).toString());
855 if (fi.exists())
856 return fi.remove();
857 return false;
858 }
859
deleteSessions(const QStringList & sessions)860 void SessionManager::deleteSessions(const QStringList &sessions)
861 {
862 for (const QString &session : sessions)
863 deleteSession(session);
864 }
865
cloneSession(const QString & original,const QString & clone)866 bool SessionManager::cloneSession(const QString &original, const QString &clone)
867 {
868 if (!d->m_sessions.contains(original))
869 return false;
870
871 QFile fi(sessionNameToFileName(original).toString());
872 // If the file does not exist, we can still clone
873 if (!fi.exists() || fi.copy(sessionNameToFileName(clone).toString())) {
874 d->m_sessions.insert(1, clone);
875 d->m_sessionDateTimes.insert(clone, sessionNameToFileName(clone).lastModified());
876 return true;
877 }
878 return false;
879 }
880
restoreValues(const PersistentSettingsReader & reader)881 void SessionManagerPrivate::restoreValues(const PersistentSettingsReader &reader)
882 {
883 const QStringList keys = reader.restoreValue(QLatin1String("valueKeys")).toStringList();
884 foreach (const QString &key, keys) {
885 QVariant value = reader.restoreValue(QLatin1String("value-") + key);
886 m_values.insert(key, value);
887 }
888 }
889
restoreDependencies(const PersistentSettingsReader & reader)890 void SessionManagerPrivate::restoreDependencies(const PersistentSettingsReader &reader)
891 {
892 QMap<QString, QVariant> depMap = reader.restoreValue(QLatin1String("ProjectDependencies")).toMap();
893 auto i = depMap.constBegin();
894 while (i != depMap.constEnd()) {
895 const QString &key = i.key();
896 QStringList values;
897 foreach (const QString &value, i.value().toStringList())
898 values << value;
899 m_depMap.insert(key, values);
900 ++i;
901 }
902 }
903
askUserAboutFailedProjects()904 void SessionManagerPrivate::askUserAboutFailedProjects()
905 {
906 QStringList failedProjects = m_failedProjects;
907 if (!failedProjects.isEmpty()) {
908 QString fileList =
909 QDir::toNativeSeparators(failedProjects.join(QLatin1String("<br>")));
910 QMessageBox box(QMessageBox::Warning,
911 SessionManager::tr("Failed to restore project files"),
912 SessionManager::tr("Could not restore the following project files:<br><b>%1</b>").
913 arg(fileList));
914 auto keepButton = new QPushButton(SessionManager::tr("Keep projects in Session"), &box);
915 auto removeButton = new QPushButton(SessionManager::tr("Remove projects from Session"), &box);
916 box.addButton(keepButton, QMessageBox::AcceptRole);
917 box.addButton(removeButton, QMessageBox::DestructiveRole);
918
919 box.exec();
920
921 if (box.clickedButton() == removeButton)
922 m_failedProjects.clear();
923 }
924 }
925
restoreStartupProject(const PersistentSettingsReader & reader)926 void SessionManagerPrivate::restoreStartupProject(const PersistentSettingsReader &reader)
927 {
928 const QString startupProject = reader.restoreValue(QLatin1String("StartupProject")).toString();
929 if (!startupProject.isEmpty()) {
930 for (Project *pro : qAsConst(m_projects)) {
931 if (pro->projectFilePath().toString() == startupProject) {
932 m_instance->setStartupProject(pro);
933 break;
934 }
935 }
936 }
937 if (!m_startupProject) {
938 if (!startupProject.isEmpty())
939 qWarning() << "Could not find startup project" << startupProject;
940 if (hasProjects())
941 m_instance->setStartupProject(m_projects.first());
942 }
943 }
944
restoreEditors(const PersistentSettingsReader & reader)945 void SessionManagerPrivate::restoreEditors(const PersistentSettingsReader &reader)
946 {
947 const QVariant editorsettings = reader.restoreValue(QLatin1String("EditorSettings"));
948 if (editorsettings.isValid()) {
949 EditorManager::restoreState(QByteArray::fromBase64(editorsettings.toByteArray()));
950 sessionLoadingProgress();
951 }
952 }
953
954 /*!
955 Loads a session, takes a session name (not filename).
956 */
restoreProjects(const QStringList & fileList)957 void SessionManagerPrivate::restoreProjects(const QStringList &fileList)
958 {
959 // indirectly adds projects to session
960 // Keep projects that failed to load in the session!
961 m_failedProjects = fileList;
962 if (!fileList.isEmpty()) {
963 ProjectExplorerPlugin::OpenProjectResult result = ProjectExplorerPlugin::openProjects(fileList);
964 if (!result)
965 ProjectExplorerPlugin::showOpenProjectError(result);
966 foreach (Project *p, result.projects())
967 m_failedProjects.removeAll(p->projectFilePath().toString());
968 }
969 }
970
971 /*
972 * ========== Notes on storing and loading the default session ==========
973 * The default session comes in two flavors: implicit and explicit. The implicit one,
974 * also referred to as "default virgin" in the code base, is the one that is active
975 * at start-up, if no session has been explicitly loaded due to command-line arguments
976 * or the "restore last session" setting in the session manager.
977 * The implicit default session silently turns into the explicit default session
978 * by loading a project or a file or changing settings in the Dependencies panel. The explicit
979 * default session can also be loaded by the user via the Welcome Screen.
980 * This mechanism somewhat complicates the handling of session-specific settings such as
981 * the ones in the task pane: Users expect that changes they make there become persistent, even
982 * when they are in the implicit default session. However, we can't just blindly store
983 * the implicit default session, because then we'd overwrite the project list of the explicit
984 * default session. Therefore, we use the following logic:
985 * - Upon start-up, if no session is to be explicitly loaded, we restore the parts of the
986 * explicit default session that are not related to projects, editors etc; the
987 * "general settings" of the session, so to speak.
988 * - When storing the implicit default session, we overwrite only these "general settings"
989 * of the explicit default session and keep the others as they are.
990 * - When switching from the implicit to the explicit default session, we keep the
991 * "general settings" and load everything else from the session file.
992 * This guarantees that user changes are properly transferred and nothing gets lost from
993 * either the implicit or the explicit default session.
994 *
995 */
loadSession(const QString & session,bool initial)996 bool SessionManager::loadSession(const QString &session, bool initial)
997 {
998 const bool loadImplicitDefault = session.isEmpty();
999 const bool switchFromImplicitToExplicitDefault = session == "default"
1000 && d->m_sessionName == "default" && !initial;
1001
1002 // Do nothing if we have that session already loaded,
1003 // exception if the session is the default virgin session
1004 // we still want to be able to load the default session
1005 if (session == d->m_sessionName && !isDefaultVirgin())
1006 return true;
1007
1008 if (!loadImplicitDefault && !sessions().contains(session))
1009 return false;
1010
1011 QStringList fileList;
1012 // Try loading the file
1013 FilePath fileName = sessionNameToFileName(loadImplicitDefault ? "default" : session);
1014 PersistentSettingsReader reader;
1015 if (fileName.exists()) {
1016 if (!reader.load(fileName)) {
1017 QMessageBox::warning(ICore::dialogParent(), tr("Error while restoring session"),
1018 tr("Could not restore session %1").arg(fileName.toUserOutput()));
1019
1020 return false;
1021 }
1022
1023 if (loadImplicitDefault) {
1024 d->restoreValues(reader);
1025 emit m_instance->sessionLoaded("default");
1026 return true;
1027 }
1028
1029 fileList = reader.restoreValue(QLatin1String("ProjectList")).toStringList();
1030 } else if (loadImplicitDefault) {
1031 return true;
1032 }
1033
1034 d->m_loadingSession = true;
1035
1036 // Allow everyone to set something in the session and before saving
1037 emit m_instance->aboutToUnloadSession(d->m_sessionName);
1038
1039 if (!save()) {
1040 d->m_loadingSession = false;
1041 return false;
1042 }
1043
1044 // Clean up
1045 if (!EditorManager::closeAllEditors()) {
1046 d->m_loadingSession = false;
1047 return false;
1048 }
1049
1050 // find a list of projects to close later
1051 const QList<Project *> projectsToRemove = Utils::filtered(projects(), [&fileList](Project *p) {
1052 return !fileList.contains(p->projectFilePath().toString());
1053 });
1054 const QList<Project *> openProjects = projects();
1055 const QStringList projectPathsToLoad = Utils::filtered(fileList, [&openProjects](const QString &path) {
1056 return !Utils::contains(openProjects, [&path](Project *p) {
1057 return p->projectFilePath().toString() == path;
1058 });
1059 });
1060 d->m_failedProjects.clear();
1061 d->m_depMap.clear();
1062 if (!switchFromImplicitToExplicitDefault)
1063 d->m_values.clear();
1064 d->m_casadeSetActive = false;
1065
1066 d->m_sessionName = session;
1067 delete d->m_writer;
1068 d->m_writer = nullptr;
1069 EditorManager::updateWindowTitles();
1070
1071 if (fileName.exists()) {
1072 d->m_virginSession = false;
1073
1074 ProgressManager::addTask(d->m_future.future(), tr("Loading Session"),
1075 "ProjectExplorer.SessionFile.Load");
1076
1077 d->m_future.setProgressRange(0, 1);
1078 d->m_future.setProgressValue(0);
1079
1080 if (!switchFromImplicitToExplicitDefault)
1081 d->restoreValues(reader);
1082 emit m_instance->aboutToLoadSession(session);
1083
1084 // retrieve all values before the following code could change them again
1085 Id modeId = Id::fromSetting(value(QLatin1String("ActiveMode")));
1086 if (!modeId.isValid())
1087 modeId = Id(Core::Constants::MODE_EDIT);
1088
1089 QColor c = QColor(reader.restoreValue(QLatin1String("Color")).toString());
1090 if (c.isValid())
1091 StyleHelper::setBaseColor(c);
1092
1093 d->m_future.setProgressRange(0, projectPathsToLoad.count() + 1/*initialization above*/ + 1/*editors*/);
1094 d->m_future.setProgressValue(1);
1095 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
1096
1097 d->restoreProjects(projectPathsToLoad);
1098 d->sessionLoadingProgress();
1099 d->restoreDependencies(reader);
1100 d->restoreStartupProject(reader);
1101
1102 removeProjects(projectsToRemove); // only remove old projects now that the startup project is set!
1103
1104 d->restoreEditors(reader);
1105
1106 d->m_future.reportFinished();
1107 d->m_future = QFutureInterface<void>();
1108
1109 // Fall back to Project mode if the startup project is unconfigured and
1110 // use the mode saved in the session otherwise
1111 if (d->m_startupProject && d->m_startupProject->needsConfiguration())
1112 modeId = Id(Constants::MODE_SESSION);
1113
1114 ModeManager::activateMode(modeId);
1115 ModeManager::setFocusToCurrentMode();
1116 } else {
1117 removeProjects(projects());
1118 ModeManager::activateMode(Id(Core::Constants::MODE_EDIT));
1119 ModeManager::setFocusToCurrentMode();
1120 }
1121
1122 d->m_casadeSetActive = reader.restoreValue(QLatin1String("CascadeSetActive"), false).toBool();
1123
1124 emit m_instance->sessionLoaded(session);
1125
1126 // Starts a event loop, better do that at the very end
1127 d->askUserAboutFailedProjects();
1128 d->m_loadingSession = false;
1129 return true;
1130 }
1131
1132 /*!
1133 Returns the last session that was opened by the user.
1134 */
lastSession()1135 QString SessionManager::lastSession()
1136 {
1137 return ICore::settings()->value(Constants::LASTSESSION_KEY).toString();
1138 }
1139
1140 /*!
1141 Returns the session that was active when Qt Creator was last closed, if any.
1142 */
startupSession()1143 QString SessionManager::startupSession()
1144 {
1145 return ICore::settings()->value(Constants::STARTUPSESSION_KEY).toString();
1146 }
1147
reportProjectLoadingProgress()1148 void SessionManager::reportProjectLoadingProgress()
1149 {
1150 d->sessionLoadingProgress();
1151 }
1152
markSessionFileDirty()1153 void SessionManager::markSessionFileDirty()
1154 {
1155 d->m_virginSession = false;
1156 }
1157
sessionLoadingProgress()1158 void SessionManagerPrivate::sessionLoadingProgress()
1159 {
1160 m_future.setProgressValue(m_future.progressValue() + 1);
1161 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
1162 }
1163
projectsForSessionName(const QString & session)1164 QStringList SessionManager::projectsForSessionName(const QString &session)
1165 {
1166 const FilePath fileName = sessionNameToFileName(session);
1167 PersistentSettingsReader reader;
1168 if (fileName.exists()) {
1169 if (!reader.load(fileName)) {
1170 qWarning() << "Could not restore session" << fileName.toUserOutput();
1171 return QStringList();
1172 }
1173 }
1174 return reader.restoreValue(QLatin1String("ProjectList")).toStringList();
1175 }
1176
1177 #ifdef WITH_TESTS
1178
testSessionSwitch()1179 void ProjectExplorerPlugin::testSessionSwitch()
1180 {
1181 QVERIFY(SessionManager::createSession("session1"));
1182 QVERIFY(SessionManager::createSession("session2"));
1183 QTemporaryFile cppFile("main.cpp");
1184 QVERIFY(cppFile.open());
1185 cppFile.close();
1186 QTemporaryFile projectFile1("XXXXXX.pro");
1187 QTemporaryFile projectFile2("XXXXXX.pro");
1188 struct SessionSpec {
1189 SessionSpec(const QString &n, QTemporaryFile &f) : name(n), projectFile(f) {}
1190 const QString name;
1191 QTemporaryFile &projectFile;
1192 };
1193 std::vector<SessionSpec> sessionSpecs{SessionSpec("session1", projectFile1),
1194 SessionSpec("session2", projectFile2)};
1195 for (const SessionSpec &sessionSpec : sessionSpecs) {
1196 static const QByteArray proFileContents
1197 = "TEMPLATE = app\n"
1198 "CONFIG -= qt\n"
1199 "SOURCES = " + cppFile.fileName().toLocal8Bit();
1200 QVERIFY(sessionSpec.projectFile.open());
1201 sessionSpec.projectFile.write(proFileContents);
1202 sessionSpec.projectFile.close();
1203 QVERIFY(SessionManager::loadSession(sessionSpec.name));
1204 const OpenProjectResult openResult
1205 = ProjectExplorerPlugin::openProject(sessionSpec.projectFile.fileName());
1206 if (openResult.errorMessage().contains("text/plain"))
1207 QSKIP("This test requires the presence of QmakeProjectManager to be fully functional");
1208 QVERIFY(openResult);
1209 QCOMPARE(openResult.projects().count(), 1);
1210 QVERIFY(openResult.project());
1211 QCOMPARE(SessionManager::projects().count(), 1);
1212 }
1213 for (int i = 0; i < 30; ++i) {
1214 QVERIFY(SessionManager::loadSession("session1"));
1215 QCOMPARE(SessionManager::activeSession(), "session1");
1216 QCOMPARE(SessionManager::projects().count(), 1);
1217 QVERIFY(SessionManager::loadSession("session2"));
1218 QCOMPARE(SessionManager::activeSession(), "session2");
1219 QCOMPARE(SessionManager::projects().count(), 1);
1220 }
1221 QVERIFY(SessionManager::loadSession("session1"));
1222 SessionManager::closeAllProjects();
1223 QVERIFY(SessionManager::loadSession("session2"));
1224 SessionManager::closeAllProjects();
1225 QVERIFY(SessionManager::deleteSession("session1"));
1226 QVERIFY(SessionManager::deleteSession("session2"));
1227 }
1228
1229 #endif // WITH_TESTS
1230
1231 } // namespace ProjectExplorer
1232