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