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 "projectwizardpage.h"
27 #include "ui_projectwizardpage.h"
28
29 #include "project.h"
30 #include "projectmodels.h"
31 #include "session.h"
32
33 #include <coreplugin/icore.h>
34 #include <coreplugin/iversioncontrol.h>
35 #include <coreplugin/iwizardfactory.h>
36 #include <coreplugin/vcsmanager.h>
37 #include <utils/algorithm.h>
38 #include <utils/fileutils.h>
39 #include <utils/qtcassert.h>
40 #include <utils/stringutils.h>
41 #include <utils/treemodel.h>
42 #include <utils/wizard.h>
43 #include <vcsbase/vcsbaseconstants.h>
44
45 #include <QDir>
46 #include <QTextStream>
47 #include <QTreeView>
48
49 /*!
50 \class ProjectExplorer::Internal::ProjectWizardPage
51
52 \brief The ProjectWizardPage class provides a wizard page showing projects
53 and version control to add new files to.
54
55 \sa ProjectExplorer::Internal::ProjectFileWizardExtension
56 */
57
58 using namespace Core;
59 using namespace Utils;
60
61 namespace ProjectExplorer {
62 namespace Internal {
63
64 class AddNewTree : public TreeItem
65 {
66 public:
67 AddNewTree(const QString &displayName);
68 AddNewTree(FolderNode *node, QList<AddNewTree *> children, const QString &displayName);
69 AddNewTree(FolderNode *node, QList<AddNewTree *> children, const FolderNode::AddNewInformation &info);
70
71 QVariant data(int column, int role) const override;
72 Qt::ItemFlags flags(int column) const override;
73
displayName() const74 QString displayName() const { return m_displayName; }
node() const75 FolderNode *node() const { return m_node; }
priority() const76 int priority() const { return m_priority; }
77
78 private:
79 QString m_displayName;
80 QString m_toolTip;
81 FolderNode *m_node = nullptr;
82 bool m_canAdd = true;
83 int m_priority = -1;
84 };
85
AddNewTree(const QString & displayName)86 AddNewTree::AddNewTree(const QString &displayName) :
87 m_displayName(displayName)
88 { }
89
90 // FIXME: potentially merge the following two functions.
91 // Note the different handling of 'node' and m_canAdd.
AddNewTree(FolderNode * node,QList<AddNewTree * > children,const QString & displayName)92 AddNewTree::AddNewTree(FolderNode *node, QList<AddNewTree *> children, const QString &displayName) :
93 m_displayName(displayName),
94 m_node(node),
95 m_canAdd(false)
96 {
97 if (node)
98 m_toolTip = node->directory();
99 foreach (AddNewTree *child, children)
100 appendChild(child);
101 }
102
AddNewTree(FolderNode * node,QList<AddNewTree * > children,const FolderNode::AddNewInformation & info)103 AddNewTree::AddNewTree(FolderNode *node, QList<AddNewTree *> children,
104 const FolderNode::AddNewInformation &info) :
105 m_displayName(info.displayName),
106 m_node(node),
107 m_priority(info.priority)
108 {
109 if (node)
110 m_toolTip = node->directory();
111 foreach (AddNewTree *child, children)
112 appendChild(child);
113 }
114
115
data(int,int role) const116 QVariant AddNewTree::data(int, int role) const
117 {
118 switch (role) {
119 case Qt::DisplayRole:
120 return m_displayName;
121 case Qt::ToolTipRole:
122 return m_toolTip;
123 case Qt::UserRole:
124 return QVariant::fromValue(static_cast<void*>(node()));
125 default:
126 return QVariant();
127 }
128 }
129
flags(int) const130 Qt::ItemFlags AddNewTree::flags(int) const
131 {
132 if (m_canAdd)
133 return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
134 return Qt::NoItemFlags;
135 }
136
137 // --------------------------------------------------------------------
138 // BestNodeSelector:
139 // --------------------------------------------------------------------
140
141 class BestNodeSelector
142 {
143 public:
144 BestNodeSelector(const QString &commonDirectory, const QStringList &files);
145 void inspect(AddNewTree *tree, bool isContextNode);
146 AddNewTree *bestChoice() const;
147 bool deploys();
148 QString deployingProjects() const;
149
150 private:
151 QString m_commonDirectory;
152 QStringList m_files;
153 bool m_deploys = false;
154 QString m_deployText;
155 AddNewTree *m_bestChoice = nullptr;
156 int m_bestMatchLength = -1;
157 int m_bestMatchPriority = -1;
158 };
159
BestNodeSelector(const QString & commonDirectory,const QStringList & files)160 BestNodeSelector::BestNodeSelector(const QString &commonDirectory, const QStringList &files) :
161 m_commonDirectory(commonDirectory),
162 m_files(files),
163 m_deployText(QCoreApplication::translate("ProjectWizard", "The files are implicitly added to the projects:") + QLatin1Char('\n'))
164 { }
165
166 // Find the project the new files should be added
167 // If any node deploys the files, then we don't want to add the files.
168 // Otherwise consider their common path. Either a direct match on the directory
169 // or the directory with the longest matching path (list containing"/project/subproject1"
170 // matching common path "/project/subproject1/newuserpath").
inspect(AddNewTree * tree,bool isContextNode)171 void BestNodeSelector::inspect(AddNewTree *tree, bool isContextNode)
172 {
173 FolderNode *node = tree->node();
174 if (node->isProjectNodeType()) {
175 if (static_cast<ProjectNode *>(node)->deploysFolder(m_commonDirectory)) {
176 m_deploys = true;
177 m_deployText += tree->displayName() + QLatin1Char('\n');
178 }
179 }
180 if (m_deploys)
181 return;
182
183 const QString projectDirectory = node->directory();
184 const int projectDirectorySize = projectDirectory.size();
185 if (m_commonDirectory != projectDirectory
186 && !m_commonDirectory.startsWith(projectDirectory + QLatin1Char('/'))
187 && !isContextNode)
188 return;
189
190 bool betterMatch = isContextNode
191 || (tree->priority() > 0
192 && (projectDirectorySize > m_bestMatchLength
193 || (projectDirectorySize == m_bestMatchLength && tree->priority() > m_bestMatchPriority)));
194
195 if (betterMatch) {
196 m_bestMatchPriority = tree->priority();
197 m_bestMatchLength = isContextNode ? std::numeric_limits<int>::max() : projectDirectorySize;
198 m_bestChoice = tree;
199 }
200 }
201
bestChoice() const202 AddNewTree *BestNodeSelector::bestChoice() const
203 {
204 if (m_deploys)
205 return nullptr;
206 return m_bestChoice;
207 }
208
deploys()209 bool BestNodeSelector::deploys()
210 {
211 return m_deploys;
212 }
213
deployingProjects() const214 QString BestNodeSelector::deployingProjects() const
215 {
216 if (m_deploys)
217 return m_deployText;
218 return QString();
219 }
220
221 // --------------------------------------------------------------------
222 // Helper:
223 // --------------------------------------------------------------------
224
createNoneNode(BestNodeSelector * selector)225 static inline AddNewTree *createNoneNode(BestNodeSelector *selector)
226 {
227 QString displayName = QCoreApplication::translate("ProjectWizard", "<None>");
228 if (selector->deploys())
229 displayName = QCoreApplication::translate("ProjectWizard", "<Implicitly Add>");
230 return new AddNewTree(displayName);
231 }
232
buildAddProjectTree(ProjectNode * root,const QString & projectPath,Node * contextNode,BestNodeSelector * selector)233 static inline AddNewTree *buildAddProjectTree(ProjectNode *root, const QString &projectPath, Node *contextNode, BestNodeSelector *selector)
234 {
235 QList<AddNewTree *> children;
236 for (Node *node : root->nodes()) {
237 if (ProjectNode *pn = node->asProjectNode()) {
238 if (AddNewTree *child = buildAddProjectTree(pn, projectPath, contextNode, selector))
239 children.append(child);
240 }
241 }
242
243 if (root->supportsAction(AddSubProject, root) && !root->supportsAction(InheritedFromParent, root)) {
244 if (projectPath.isEmpty() || root->canAddSubProject(projectPath)) {
245 FolderNode::AddNewInformation info = root->addNewInformation(QStringList() << projectPath, contextNode);
246 auto item = new AddNewTree(root, children, info);
247 selector->inspect(item, root == contextNode);
248 return item;
249 }
250 }
251
252 if (children.isEmpty())
253 return nullptr;
254 return new AddNewTree(root, children, root->displayName());
255 }
256
buildAddFilesTree(FolderNode * root,const QStringList & files,Node * contextNode,BestNodeSelector * selector)257 static inline AddNewTree *buildAddFilesTree(FolderNode *root, const QStringList &files,
258 Node *contextNode, BestNodeSelector *selector)
259 {
260 QList<AddNewTree *> children;
261 foreach (FolderNode *fn, root->folderNodes()) {
262 AddNewTree *child = buildAddFilesTree(fn, files, contextNode, selector);
263 if (child)
264 children.append(child);
265 }
266
267 if (root->supportsAction(AddNewFile, root) && !root->supportsAction(InheritedFromParent, root)) {
268 FolderNode::AddNewInformation info = root->addNewInformation(files, contextNode);
269 auto item = new AddNewTree(root, children, info);
270 selector->inspect(item, root == contextNode);
271 return item;
272 }
273 if (children.isEmpty())
274 return nullptr;
275 return new AddNewTree(root, children, root->displayName());
276 }
277
278 // --------------------------------------------------------------------
279 // ProjectWizardPage:
280 // --------------------------------------------------------------------
281
ProjectWizardPage(QWidget * parent)282 ProjectWizardPage::ProjectWizardPage(QWidget *parent) : WizardPage(parent),
283 m_ui(new Ui::WizardPage)
284 {
285 m_ui->setupUi(this);
286 m_ui->vcsManageButton->setText(ICore::msgShowOptionsDialog());
287 connect(m_ui->projectComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
288 this, &ProjectWizardPage::projectChanged);
289 connect(m_ui->addToVersionControlComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
290 this, &ProjectWizardPage::versionControlChanged);
291 connect(m_ui->vcsManageButton, &QAbstractButton::clicked, this, &ProjectWizardPage::manageVcs);
292 setProperty(SHORT_TITLE_PROPERTY, tr("Summary"));
293
294 connect(VcsManager::instance(), &VcsManager::configurationChanged,
295 this, &ProjectExplorer::Internal::ProjectWizardPage::initializeVersionControls);
296
297 m_ui->projectComboBox->setModel(&m_model);
298 }
299
~ProjectWizardPage()300 ProjectWizardPage::~ProjectWizardPage()
301 {
302 disconnect(m_ui->projectComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
303 this, &ProjectWizardPage::projectChanged);
304 delete m_ui;
305 }
306
expandTree(const QModelIndex & root)307 bool ProjectWizardPage::expandTree(const QModelIndex &root)
308 {
309 bool expand = false;
310 if (!root.isValid()) // always expand root
311 expand = true;
312
313 // Check children
314 int count = m_model.rowCount(root);
315 for (int i = 0; i < count; ++i) {
316 if (expandTree(m_model.index(i, 0, root)))
317 expand = true;
318 }
319
320 // Apply to self
321 if (expand)
322 m_ui->projectComboBox->view()->expand(root);
323 else
324 m_ui->projectComboBox->view()->collapse(root);
325
326 // if we are a high priority node, our *parent* needs to be expanded
327 auto tree = static_cast<AddNewTree *>(root.internalPointer());
328 if (tree && tree->priority() >= 100)
329 expand = true;
330
331 return expand;
332 }
333
setBestNode(AddNewTree * tree)334 void ProjectWizardPage::setBestNode(AddNewTree *tree)
335 {
336 QModelIndex index = tree ? m_model.indexForItem(tree) : QModelIndex();
337 m_ui->projectComboBox->setCurrentIndex(index);
338
339 while (index.isValid()) {
340 m_ui->projectComboBox->view()->expand(index);
341 index = index.parent();
342 }
343 }
344
currentNode() const345 FolderNode *ProjectWizardPage::currentNode() const
346 {
347 QVariant v = m_ui->projectComboBox->currentData(Qt::UserRole);
348 return v.isNull() ? nullptr : static_cast<FolderNode *>(v.value<void *>());
349 }
350
setAddingSubProject(bool addingSubProject)351 void ProjectWizardPage::setAddingSubProject(bool addingSubProject)
352 {
353 m_ui->projectLabel->setText(addingSubProject ?
354 tr("Add as a subproject to project:")
355 : tr("Add to &project:"));
356 }
357
initializeVersionControls()358 void ProjectWizardPage::initializeVersionControls()
359 {
360 // Figure out version control situation:
361 // 0) Check that any version control is available
362 // 1) Directory is managed and VCS supports "Add" -> List it
363 // 2) Directory is managed and VCS does not support "Add" -> None available
364 // 3) Directory is not managed -> Offer all VCS that support "CreateRepository"
365
366 QList<IVersionControl *> versionControls = VcsManager::versionControls();
367 if (versionControls.isEmpty())
368 hideVersionControlUiElements();
369
370 IVersionControl *currentSelection = nullptr;
371 int currentIdx = versionControlIndex() - 1;
372 if (currentIdx >= 0 && currentIdx <= m_activeVersionControls.size() - 1)
373 currentSelection = m_activeVersionControls.at(currentIdx);
374
375 m_activeVersionControls.clear();
376
377 QStringList versionControlChoices = QStringList(tr("<None>"));
378 if (!m_commonDirectory.isEmpty()) {
379 IVersionControl *managingControl = VcsManager::findVersionControlForDirectory(m_commonDirectory);
380 if (managingControl) {
381 // Under VCS
382 if (managingControl->supportsOperation(IVersionControl::AddOperation)) {
383 versionControlChoices.append(managingControl->displayName());
384 m_activeVersionControls.push_back(managingControl);
385 m_repositoryExists = true;
386 }
387 } else {
388 // Create
389 foreach (IVersionControl *vc, VcsManager::versionControls()) {
390 if (vc->supportsOperation(IVersionControl::CreateRepositoryOperation)) {
391 versionControlChoices.append(vc->displayName());
392 m_activeVersionControls.append(vc);
393 }
394 }
395 m_repositoryExists = false;
396 }
397 } // has a common root.
398
399 setVersionControls(versionControlChoices);
400 // Enable adding to version control by default.
401 if (m_repositoryExists && versionControlChoices.size() >= 2)
402 setVersionControlIndex(1);
403 if (!m_repositoryExists) {
404 int newIdx = m_activeVersionControls.indexOf(currentSelection) + 1;
405 setVersionControlIndex(newIdx);
406 }
407 }
408
runVersionControl(const QList<GeneratedFile> & files,QString * errorMessage)409 bool ProjectWizardPage::runVersionControl(const QList<GeneratedFile> &files, QString *errorMessage)
410 {
411 // Add files to version control (Entry at 0 is 'None').
412 const int vcsIndex = versionControlIndex() - 1;
413 if (vcsIndex < 0 || vcsIndex >= m_activeVersionControls.size())
414 return true;
415 QTC_ASSERT(!m_commonDirectory.isEmpty(), return false);
416
417 IVersionControl *versionControl = m_activeVersionControls.at(vcsIndex);
418 // Create repository?
419 if (!m_repositoryExists) {
420 QTC_ASSERT(versionControl->supportsOperation(IVersionControl::CreateRepositoryOperation), return false);
421 if (!versionControl->vcsCreateRepository(m_commonDirectory)) {
422 *errorMessage = tr("A version control system repository could not be created in \"%1\".").arg(m_commonDirectory);
423 return false;
424 }
425 }
426 // Add files if supported.
427 if (versionControl->supportsOperation(IVersionControl::AddOperation)) {
428 foreach (const GeneratedFile &generatedFile, files) {
429 if (!versionControl->vcsAdd(generatedFile.path())) {
430 *errorMessage = tr("Failed to add \"%1\" to the version control system.").arg(generatedFile.path());
431 return false;
432 }
433 }
434 }
435 return true;
436 }
437
initializeProjectTree(Node * context,const QStringList & paths,IWizardFactory::WizardKind kind,ProjectAction action)438 void ProjectWizardPage::initializeProjectTree(Node *context, const QStringList &paths,
439 IWizardFactory::WizardKind kind,
440 ProjectAction action)
441 {
442 BestNodeSelector selector(m_commonDirectory, paths);
443
444 TreeItem *root = m_model.rootItem();
445 root->removeChildren();
446 for (Project *project : SessionManager::projects()) {
447 if (ProjectNode *pn = project->rootProjectNode()) {
448 if (kind == IWizardFactory::ProjectWizard) {
449 if (AddNewTree *child = buildAddProjectTree(pn, paths.first(), context, &selector))
450 root->appendChild(child);
451 } else {
452 if (AddNewTree *child = buildAddFilesTree(pn, paths, context, &selector))
453 root->appendChild(child);
454 }
455 }
456 }
457 root->sortChildren([](const TreeItem *ti1, const TreeItem *ti2) {
458 return compareNodes(static_cast<const AddNewTree *>(ti1)->node(),
459 static_cast<const AddNewTree *>(ti2)->node());
460 });
461 root->prependChild(createNoneNode(&selector));
462
463 // Set combobox to context node if that appears in the tree:
464 auto predicate = [context](TreeItem *ti) { return static_cast<AddNewTree*>(ti)->node() == context; };
465 TreeItem *contextItem = root->findAnyChild(predicate);
466 if (contextItem)
467 m_ui->projectComboBox->setCurrentIndex(m_model.indexForItem(contextItem));
468
469 setAdditionalInfo(selector.deployingProjects());
470 setBestNode(selector.bestChoice());
471 setAddingSubProject(action == AddSubProject);
472
473 m_ui->projectComboBox->setEnabled(m_model.rowCount(QModelIndex()) > 1);
474 }
475
setNoneLabel(const QString & label)476 void ProjectWizardPage::setNoneLabel(const QString &label)
477 {
478 m_ui->projectComboBox->setItemText(0, label);
479 }
480
setAdditionalInfo(const QString & text)481 void ProjectWizardPage::setAdditionalInfo(const QString &text)
482 {
483 m_ui->additionalInfo->setText(text);
484 m_ui->additionalInfo->setVisible(!text.isEmpty());
485 }
486
setVersionControls(const QStringList & vcs)487 void ProjectWizardPage::setVersionControls(const QStringList &vcs)
488 {
489 m_ui->addToVersionControlComboBox->clear();
490 m_ui->addToVersionControlComboBox->addItems(vcs);
491 }
492
versionControlIndex() const493 int ProjectWizardPage::versionControlIndex() const
494 {
495 return m_ui->addToVersionControlComboBox->currentIndex();
496 }
497
setVersionControlIndex(int idx)498 void ProjectWizardPage::setVersionControlIndex(int idx)
499 {
500 m_ui->addToVersionControlComboBox->setCurrentIndex(idx);
501 }
502
currentVersionControl()503 IVersionControl *ProjectWizardPage::currentVersionControl()
504 {
505 int index = m_ui->addToVersionControlComboBox->currentIndex() - 1; // Subtract "<None>"
506 if (index < 0 || index > m_activeVersionControls.count())
507 return nullptr; // <None>
508 return m_activeVersionControls.at(index);
509 }
510
setFiles(const QStringList & fileNames)511 void ProjectWizardPage::setFiles(const QStringList &fileNames)
512 {
513 if (fileNames.count() == 1)
514 m_commonDirectory = QFileInfo(fileNames.first()).absolutePath();
515 else
516 m_commonDirectory = Utils::commonPath(fileNames);
517 QString fileMessage;
518 {
519 QTextStream str(&fileMessage);
520 str << "<qt>"
521 << (m_commonDirectory.isEmpty() ? tr("Files to be added:") : tr("Files to be added in"))
522 << "<pre>";
523
524 QStringList formattedFiles;
525 if (m_commonDirectory.isEmpty()) {
526 formattedFiles = fileNames;
527 } else {
528 str << QDir::toNativeSeparators(m_commonDirectory) << ":\n\n";
529 int prefixSize = m_commonDirectory.size();
530 if (!m_commonDirectory.endsWith('/'))
531 ++prefixSize;
532 formattedFiles = Utils::transform(fileNames, [prefixSize](const QString &f)
533 { return f.mid(prefixSize); });
534 }
535 // Alphabetically, and files in sub-directories first
536 Utils::sort(formattedFiles, [](const QString &filePath1, const QString &filePath2) -> bool {
537 const bool filePath1HasDir = filePath1.contains(QLatin1Char('/'));
538 const bool filePath2HasDir = filePath2.contains(QLatin1Char('/'));
539
540 if (filePath1HasDir == filePath2HasDir)
541 return FilePath::fromString(filePath1) < FilePath::fromString(filePath2);
542 return filePath1HasDir;
543 }
544 );
545
546 foreach (const QString &f, formattedFiles)
547 str << QDir::toNativeSeparators(f) << '\n';
548
549 str << "</pre>";
550 }
551 m_ui->filesLabel->setText(fileMessage);
552 }
553
setProjectToolTip(const QString & tt)554 void ProjectWizardPage::setProjectToolTip(const QString &tt)
555 {
556 m_ui->projectComboBox->setToolTip(tt);
557 m_ui->projectLabel->setToolTip(tt);
558 }
559
projectChanged(int index)560 void ProjectWizardPage::projectChanged(int index)
561 {
562 setProjectToolTip(index >= 0 && index < m_projectToolTips.size() ?
563 m_projectToolTips.at(index) : QString());
564 emit projectNodeChanged();
565 }
566
manageVcs()567 void ProjectWizardPage::manageVcs()
568 {
569 ICore::showOptionsDialog(VcsBase::Constants::VCS_COMMON_SETTINGS_ID, this);
570 }
571
hideVersionControlUiElements()572 void ProjectWizardPage::hideVersionControlUiElements()
573 {
574 m_ui->addToVersionControlLabel->hide();
575 m_ui->vcsManageButton->hide();
576 m_ui->addToVersionControlComboBox->hide();
577 }
578
setProjectUiVisible(bool visible)579 void ProjectWizardPage::setProjectUiVisible(bool visible)
580 {
581 m_ui->projectLabel->setVisible(visible);
582 m_ui->projectComboBox->setVisible(visible);
583 }
584
585 } // namespace Internal
586 } // namespace ProjectExplorer
587