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