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 "projecttree.h"
27 
28 #include "project.h"
29 #include "projectexplorerconstants.h"
30 #include "projectnodes.h"
31 #include "projecttreewidget.h"
32 #include "session.h"
33 #include "target.h"
34 
35 #include <coreplugin/actionmanager/actioncontainer.h>
36 #include <coreplugin/actionmanager/actionmanager.h>
37 #include <coreplugin/documentmanager.h>
38 #include <coreplugin/editormanager/editormanager.h>
39 #include <coreplugin/editormanager/ieditor.h>
40 #include <coreplugin/icore.h>
41 #include <coreplugin/idocument.h>
42 #include <coreplugin/modemanager.h>
43 #include <coreplugin/navigationwidget.h>
44 #include <coreplugin/vcsmanager.h>
45 
46 #include <utils/algorithm.h>
47 #include <utils/infobar.h>
48 #include <utils/qtcassert.h>
49 
50 #include <QApplication>
51 #include <QFileInfo>
52 #include <QMenu>
53 #include <QTimer>
54 
55 namespace { const char EXTERNAL_FILE_WARNING[] = "ExternalFile"; }
56 
57 using namespace Utils;
58 
59 namespace ProjectExplorer {
60 
61 using namespace Internal;
62 
63 ProjectTree *ProjectTree::s_instance = nullptr;
64 
ProjectTree(QObject * parent)65 ProjectTree::ProjectTree(QObject *parent) : QObject(parent)
66 {
67     s_instance = this;
68 
69     connect(Core::EditorManager::instance(), &Core::EditorManager::currentEditorChanged,
70             this, &ProjectTree::update);
71 
72     connect(qApp, &QApplication::focusChanged,
73             this, &ProjectTree::update);
74 
75     connect(SessionManager::instance(), &SessionManager::projectAdded,
76             this, &ProjectTree::sessionAndTreeChanged);
77     connect(SessionManager::instance(), &SessionManager::projectRemoved,
78             this, &ProjectTree::sessionAndTreeChanged);
79     connect(SessionManager::instance(), &SessionManager::startupProjectChanged,
80             this, &ProjectTree::sessionChanged);
81     connect(this, &ProjectTree::subtreeChanged, this, &ProjectTree::treeChanged);
82 }
83 
~ProjectTree()84 ProjectTree::~ProjectTree()
85 {
86     QTC_ASSERT(s_instance == this, return);
87     s_instance = nullptr;
88 }
89 
aboutToShutDown()90 void ProjectTree::aboutToShutDown()
91 {
92     disconnect(qApp, &QApplication::focusChanged,
93                s_instance, &ProjectTree::update);
94     s_instance->setCurrent(nullptr, nullptr);
95     qDeleteAll(s_instance->m_projectTreeWidgets);
96     QTC_CHECK(s_instance->m_projectTreeWidgets.isEmpty());
97 }
98 
instance()99 ProjectTree *ProjectTree::instance()
100 {
101     return s_instance;
102 }
103 
currentProject()104 Project *ProjectTree::currentProject()
105 {
106     return s_instance->m_currentProject;
107 }
108 
currentTarget()109 Target *ProjectTree::currentTarget()
110 {
111     Project *p = currentProject();
112     return p ? p->activeTarget() : nullptr;
113 }
114 
currentBuildSystem()115 BuildSystem *ProjectTree::currentBuildSystem()
116 {
117     Target *t = currentTarget();
118     return t ? t->buildSystem() : nullptr;
119 }
120 
currentNode()121 Node *ProjectTree::currentNode()
122 {
123     s_instance->update();
124     return s_instance->m_currentNode;
125 }
126 
currentFilePath()127 FilePath ProjectTree::currentFilePath()
128 {
129     Node *node = currentNode();
130     return node ? node->filePath() : FilePath();
131 }
132 
registerWidget(ProjectTreeWidget * widget)133 void ProjectTree::registerWidget(ProjectTreeWidget *widget)
134 {
135     s_instance->m_projectTreeWidgets.append(widget);
136     if (hasFocus(widget))
137         s_instance->updateFromProjectTreeWidget(widget);
138 }
139 
unregisterWidget(ProjectTreeWidget * widget)140 void ProjectTree::unregisterWidget(ProjectTreeWidget *widget)
141 {
142     s_instance->m_projectTreeWidgets.removeOne(widget);
143     if (hasFocus(widget))
144         s_instance->updateFromDocumentManager();
145 }
146 
nodeChanged(ProjectTreeWidget * widget)147 void ProjectTree::nodeChanged(ProjectTreeWidget *widget)
148 {
149     if (hasFocus(widget))
150         s_instance->updateFromProjectTreeWidget(widget);
151 }
152 
update()153 void ProjectTree::update()
154 {
155     ProjectTreeWidget *focus = m_focusForContextMenu;
156     if (!focus)
157         focus = currentWidget();
158 
159     if (focus)
160         updateFromProjectTreeWidget(focus);
161     else
162         updateFromDocumentManager();
163 }
164 
updateFromProjectTreeWidget(ProjectTreeWidget * widget)165 void ProjectTree::updateFromProjectTreeWidget(ProjectTreeWidget *widget)
166 {
167     Node *currentNode = widget->currentNode();
168     Project *project = projectForNode(currentNode);
169 
170     if (!project)
171         updateFromNode(nullptr); // Project was removed!
172     else
173         setCurrent(currentNode, project);
174 }
175 
updateFromDocumentManager()176 void ProjectTree::updateFromDocumentManager()
177 {
178     if (Core::IDocument *document = Core::EditorManager::currentDocument()) {
179         const FilePath fileName = document->filePath();
180         updateFromNode(ProjectTreeWidget::nodeForFile(fileName));
181     } else {
182         updateFromNode(nullptr);
183     }
184 }
185 
updateFromNode(Node * node)186 void ProjectTree::updateFromNode(Node *node)
187 {
188     Project *project;
189     if (node)
190         project = projectForNode(node);
191     else
192         project = SessionManager::startupProject();
193 
194     setCurrent(node, project);
195     foreach (ProjectTreeWidget *widget, m_projectTreeWidgets)
196         widget->sync(node);
197 }
198 
setCurrent(Node * node,Project * project)199 void ProjectTree::setCurrent(Node *node, Project *project)
200 {
201     const bool changedProject = project != m_currentProject;
202     if (changedProject) {
203         if (m_currentProject) {
204             disconnect(m_currentProject, &Project::projectLanguagesUpdated,
205                        this, &ProjectTree::updateContext);
206         }
207 
208         m_currentProject = project;
209 
210         if (m_currentProject) {
211             connect(m_currentProject, &Project::projectLanguagesUpdated,
212                     this, &ProjectTree::updateContext);
213         }
214     }
215 
216     if (Core::IDocument *document = Core::EditorManager::currentDocument()) {
217         if (node) {
218             disconnect(document, &Core::IDocument::changed,
219                        this, &ProjectTree::updateExternalFileWarning);
220             document->infoBar()->removeInfo(EXTERNAL_FILE_WARNING);
221         } else {
222             connect(document, &Core::IDocument::changed,
223                     this, &ProjectTree::updateExternalFileWarning,
224                     Qt::UniqueConnection);
225         }
226     }
227 
228     if (node != m_currentNode) {
229         m_currentNode = node;
230         emit currentNodeChanged(node);
231     }
232 
233     if (changedProject) {
234         emit currentProjectChanged(m_currentProject);
235         sessionChanged();
236         updateContext();
237     }
238 }
239 
sessionChanged()240 void ProjectTree::sessionChanged()
241 {
242     if (m_currentProject) {
243         Core::DocumentManager::setDefaultLocationForNewFiles(m_currentProject->projectDirectory().toString());
244     } else if (Project *project = SessionManager::startupProject()) {
245         Core::DocumentManager::setDefaultLocationForNewFiles(project->projectDirectory().toString());
246         updateFromNode(nullptr); // Make startup project current if there is no other current
247     } else {
248         Core::DocumentManager::setDefaultLocationForNewFiles(QString());
249     }
250     update();
251 }
252 
updateContext()253 void ProjectTree::updateContext()
254 {
255     Core::Context oldContext;
256     oldContext.add(m_lastProjectContext);
257 
258     Core::Context newContext;
259     if (m_currentProject) {
260         newContext.add(m_currentProject->projectContext());
261         newContext.add(m_currentProject->projectLanguages());
262 
263         m_lastProjectContext = newContext;
264     } else {
265         m_lastProjectContext = Core::Context();
266     }
267 
268     Core::ICore::updateAdditionalContexts(oldContext, newContext);
269 }
270 
emitSubtreeChanged(FolderNode * node)271 void ProjectTree::emitSubtreeChanged(FolderNode *node)
272 {
273     if (hasNode(node))
274         emit s_instance->subtreeChanged(node);
275 }
276 
sessionAndTreeChanged()277 void ProjectTree::sessionAndTreeChanged()
278 {
279     sessionChanged();
280     emit treeChanged();
281 }
282 
expandCurrentNodeRecursively()283 void ProjectTree::expandCurrentNodeRecursively()
284 {
285     if (const auto w = currentWidget())
286         w->expandCurrentNodeRecursively();
287 }
288 
collapseAll()289 void ProjectTree::collapseAll()
290 {
291     if (const auto w = currentWidget())
292         w->collapseAll();
293 }
294 
expandAll()295 void ProjectTree::expandAll()
296 {
297     if (const auto w = currentWidget())
298         w->expandAll();
299 }
300 
changeProjectRootDirectory()301 void ProjectTree::changeProjectRootDirectory()
302 {
303     if (m_currentProject)
304         m_currentProject->changeRootProjectDirectory();
305 }
306 
updateExternalFileWarning()307 void ProjectTree::updateExternalFileWarning()
308 {
309     auto document = qobject_cast<Core::IDocument *>(sender());
310     if (!document || document->filePath().isEmpty())
311         return;
312     Utils::InfoBar *infoBar = document->infoBar();
313     Utils::Id externalFileId(EXTERNAL_FILE_WARNING);
314     if (!document->isModified()) {
315         infoBar->removeInfo(externalFileId);
316         return;
317     }
318     if (!infoBar->canInfoBeAdded(externalFileId))
319         return;
320     const FilePath fileName = document->filePath();
321     const QList<Project *> projects = SessionManager::projects();
322     if (projects.isEmpty())
323         return;
324     for (Project *project : projects) {
325         FilePath projectDir = project->projectDirectory();
326         if (projectDir.isEmpty())
327             continue;
328         if (fileName.isChildOf(projectDir))
329             return;
330         // External file. Test if it under the same VCS
331         QString topLevel;
332         if (Core::VcsManager::findVersionControlForDirectory(projectDir.toString(), &topLevel)
333                 && fileName.isChildOf(FilePath::fromString(topLevel))) {
334             return;
335         }
336     }
337     infoBar->addInfo(
338         Utils::InfoBarEntry(externalFileId,
339                             tr("<b>Warning:</b> This file is outside the project directory."),
340                             Utils::InfoBarEntry::GlobalSuppression::Enabled));
341 }
342 
hasFocus(ProjectTreeWidget * widget)343 bool ProjectTree::hasFocus(ProjectTreeWidget *widget)
344 {
345     return widget
346             && ((widget->focusWidget() && widget->focusWidget()->hasFocus())
347                 || s_instance->m_focusForContextMenu == widget);
348 }
349 
currentWidget() const350 ProjectTreeWidget *ProjectTree::currentWidget() const
351 {
352     return findOrDefault(m_projectTreeWidgets, &ProjectTree::hasFocus);
353 }
354 
showContextMenu(ProjectTreeWidget * focus,const QPoint & globalPos,Node * node)355 void ProjectTree::showContextMenu(ProjectTreeWidget *focus, const QPoint &globalPos, Node *node)
356 {
357     QMenu *contextMenu = nullptr;
358     emit s_instance->aboutToShowContextMenu(node);
359 
360     if (!node) {
361         contextMenu = Core::ActionManager::actionContainer(Constants::M_SESSIONCONTEXT)->menu();
362     } else  if (node->isProjectNodeType()) {
363         if ((node->parentFolderNode() && node->parentFolderNode()->asContainerNode())
364                 || node->asContainerNode())
365             contextMenu = Core::ActionManager::actionContainer(Constants::M_PROJECTCONTEXT)->menu();
366         else
367             contextMenu = Core::ActionManager::actionContainer(Constants::M_SUBPROJECTCONTEXT)->menu();
368     } else if (node->isVirtualFolderType() || node->isFolderNodeType()) {
369         contextMenu = Core::ActionManager::actionContainer(Constants::M_FOLDERCONTEXT)->menu();
370     } else if (node->asFileNode()) {
371         contextMenu = Core::ActionManager::actionContainer(Constants::M_FILECONTEXT)->menu();
372     }
373 
374     if (contextMenu && !contextMenu->actions().isEmpty()) {
375         s_instance->m_focusForContextMenu = focus;
376         contextMenu->popup(globalPos);
377         connect(contextMenu, &QMenu::aboutToHide,
378                 s_instance, &ProjectTree::hideContextMenu,
379                 Qt::ConnectionType(Qt::UniqueConnection | Qt::QueuedConnection));
380     }
381 }
382 
highlightProject(Project * project,const QString & message)383 void ProjectTree::highlightProject(Project *project, const QString &message)
384 {
385     Core::ModeManager::activateMode(Core::Constants::MODE_EDIT);
386 
387     // Shows and focusses a project tree
388     QWidget *widget = Core::NavigationWidget::activateSubWidget(ProjectExplorer::Constants::PROJECTTREE_ID, Core::Side::Left);
389 
390     if (auto *projectTreeWidget = qobject_cast<ProjectTreeWidget *>(widget))
391         projectTreeWidget->showMessage(project->rootProjectNode(), message);
392 }
393 
394 /*!
395     Registers the function \a treeChange to be run on a (sub tree of the)
396     project tree when it is created. The function must be thread-safe, and
397     applying the function on the same tree a second time must be a no-op.
398 */
registerTreeManager(const TreeManagerFunction & treeChange)399 void ProjectTree::registerTreeManager(const TreeManagerFunction &treeChange)
400 {
401     if (treeChange)
402         s_instance->m_treeManagers.append(treeChange);
403 }
404 
applyTreeManager(FolderNode * folder)405 void ProjectTree::applyTreeManager(FolderNode *folder)
406 {
407     if (!folder)
408         return;
409 
410     for (TreeManagerFunction &f : s_instance->m_treeManagers)
411         f(folder);
412 }
413 
hasNode(const Node * node)414 bool ProjectTree::hasNode(const Node *node)
415 {
416     return Utils::contains(SessionManager::projects(), [node](const Project *p) {
417         if (!p)
418             return false;
419         if (p->containerNode() == node)
420             return true;
421         // When parsing fails we have a living container node but no rootProjectNode.
422         ProjectNode *pn = p->rootProjectNode();
423         if (!pn)
424             return false;
425         return pn->findNode([node](const Node *n) { return n == node; }) != nullptr;
426     });
427 }
428 
forEachNode(const std::function<void (Node *)> & task)429 void ProjectTree::forEachNode(const std::function<void(Node *)> &task)
430 {
431     const QList<Project *> projects = SessionManager::projects();
432     for (Project *project : projects) {
433         if (ProjectNode *projectNode = project->rootProjectNode()) {
434             task(projectNode);
435             projectNode->forEachGenericNode(task);
436         }
437     }
438 }
439 
projectForNode(const Node * node)440 Project *ProjectTree::projectForNode(const Node *node)
441 {
442     if (!node)
443         return nullptr;
444 
445     const FolderNode *folder = node->asFolderNode();
446     if (!folder)
447         folder = node->parentFolderNode();
448 
449     while (folder && folder->parentFolderNode())
450         folder = folder->parentFolderNode();
451 
452     return Utils::findOrDefault(SessionManager::projects(), [folder](const Project *pro) {
453         return pro->containerNode() == folder;
454     });
455 }
456 
nodeForFile(const FilePath & fileName)457 Node *ProjectTree::nodeForFile(const FilePath &fileName)
458 {
459     Node *node = nullptr;
460     for (const Project *project : SessionManager::projects()) {
461         project->nodeForFilePath(fileName, [&](const Node *n) {
462             if (!node || (!node->asFileNode() && n->asFileNode()))
463                 node = const_cast<Node *>(n);
464             return false;
465         });
466         // early return:
467         if (node && node->asFileNode())
468             return node;
469     }
470     return node;
471 }
472 
siblingsWithSameBaseName(const Node * fileNode)473 const QList<Node *> ProjectTree::siblingsWithSameBaseName(const Node *fileNode)
474 {
475     ProjectNode *productNode = fileNode->parentProjectNode();
476     while (productNode && !productNode->isProduct())
477         productNode = productNode->parentProjectNode();
478     if (!productNode)
479         return {};
480     const QFileInfo fi = fileNode->filePath().toFileInfo();
481     const auto filter = [&fi](const Node *n) {
482         return n->asFileNode()
483                 && n->filePath().toFileInfo().dir() == fi.dir()
484                 && n->filePath().completeBaseName() == fi.completeBaseName()
485                 && n->filePath().toString() != fi.filePath();
486     };
487     return productNode->findNodes(filter);
488 }
489 
hideContextMenu()490 void ProjectTree::hideContextMenu()
491 {
492     if (m_keepCurrentNodeRequests == 0)
493         m_focusForContextMenu = nullptr;
494 }
495 
CurrentNodeKeeper()496 ProjectTree::CurrentNodeKeeper::CurrentNodeKeeper()
497     : m_active(ProjectTree::instance()->m_focusForContextMenu)
498 {
499     if (m_active)
500         ++ProjectTree::instance()->m_keepCurrentNodeRequests;
501 }
502 
~CurrentNodeKeeper()503 ProjectTree::CurrentNodeKeeper::~CurrentNodeKeeper()
504 {
505     if (m_active && --ProjectTree::instance()->m_keepCurrentNodeRequests == 0) {
506         ProjectTree::instance()->m_focusForContextMenu = nullptr;
507         ProjectTree::instance()->update();
508     }
509 }
510 
511 } // namespace ProjectExplorer
512