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