1 /**************************************************************************
3 ** This file is part of Qt Creator
5 ** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies).
7 ** Contact: Nokia Corporation (qt-info@nokia.com)
10 ** GNU Lesser General Public License Usage
12 ** This file may be used under the terms of the GNU Lesser General Public
13 ** License version 2.1 as published by the Free Software Foundation and
14 ** appearing in the file LICENSE.LGPL included in the packaging of this file.
15 ** Please review the following information to ensure the GNU Lesser General
16 ** Public License version 2.1 requirements will be met:
17 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
19 ** In addition, as a special exception, Nokia gives you certain additional
20 ** rights. These rights are described in the Nokia Qt LGPL Exception
21 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
25 ** Alternatively, this file may be used in accordance with the terms and
26 ** conditions contained in a signed written agreement between you and Nokia.
28 ** If you have questions regarding the use of this file, please contact
29 ** Nokia at qt-info@nokia.com.
31 **************************************************************************/
33 #include "projectfilewizardextension.h"
34 #include "projectexplorer.h"
36 #include "projectnodes.h"
37 #include "nodesvisitor.h"
38 #include "projectwizardpage.h"
40 #include <utils/qtcassert.h>
41 #include <utils/stringutils.h>
43 #include <coreplugin/basefilewizard.h>
44 #include <coreplugin/documentmanager.h>
45 #include <coreplugin/icore.h>
46 #include <coreplugin/iversioncontrol.h>
47 #include <coreplugin/vcsmanager.h>
48 #include <coreplugin/mimedatabase.h>
49 #include <extensionsystem/pluginmanager.h>
50 #include <texteditor/texteditorsettings.h>
51 #include <texteditor/indenter.h>
52 #include <texteditor/icodestylepreferences.h>
53 #include <texteditor/icodestylepreferencesfactory.h>
54 #include <texteditor/normalindenter.h>
55 #include <texteditor/tabsettings.h>
56 #include <projectexplorer/project.h>
57 #include <projectexplorer/editorconfiguration.h>
60 #include <QtAlgorithms>
65 #include <QTextDocument>
66 #include <QTextCursor>
69 \class ProjectExplorer::Internal::ProjectFileWizardExtension
71 \brief Post-file generating steps of a project wizard.
75 \o Add to a project file (*.pri/ *.pro)
76 \o Initialize a version control repository (unless the path is already
77 managed) and do 'add' if the VCS supports it.
80 \sa ProjectExplorer::Internal::ProjectWizardPage
83 enum { debugExtension = 0 };
85 namespace ProjectExplorer {
87 typedef QList<ProjectNode *> ProjectNodeList;
91 // AllProjectNodesVisitor: Retrieve all projects (*.pri/*.pro)
92 // which support adding files
93 class AllProjectNodesVisitor : public NodesVisitor
96 AllProjectNodesVisitor(ProjectNode::ProjectAction action)
100 static ProjectNodeList allProjects(ProjectNode::ProjectAction action);
102 virtual void visitProjectNode(ProjectNode *node);
105 ProjectNodeList m_projectNodes;
106 ProjectNode::ProjectAction m_action;
109 ProjectNodeList AllProjectNodesVisitor::allProjects(ProjectNode::ProjectAction action)
111 AllProjectNodesVisitor visitor(action);
112 ProjectExplorerPlugin::instance()->session()->sessionNode()->accept(&visitor);
113 return visitor.m_projectNodes;
116 void AllProjectNodesVisitor::visitProjectNode(ProjectNode *node)
118 if (node->supportedActions(node).contains(m_action))
119 m_projectNodes.push_back(node);
122 // ProjectEntry: Context entry for a *.pri/*.pro file. Stores name and path
123 // for quick sort and path search, provides operator<() for maps.
124 struct ProjectEntry {
125 enum Type { ProFile, PriFile }; // Sort order: 'pro' before 'pri'
127 ProjectEntry() : node(0), type(ProFile) {}
128 explicit ProjectEntry(ProjectNode *node);
130 int compare(const ProjectEntry &rhs) const;
133 QString directory; // For matching against wizards' files, which are native.
139 ProjectEntry::ProjectEntry(ProjectNode *n) :
143 const QFileInfo fi(node->path());
144 fileName = fi.fileName();
145 baseName = fi.baseName();
146 if (fi.suffix() != QLatin1String("pro"))
148 directory = fi.absolutePath();
151 // Sort helper that sorts by base name and puts '*.pro' before '*.pri'
152 int ProjectEntry::compare(const ProjectEntry &rhs) const
154 if (const int drc = directory.compare(rhs.directory))
156 if (const int brc = baseName.compare(rhs.baseName))
165 inline bool operator<(const ProjectEntry &pe1, const ProjectEntry &pe2)
167 return pe1.compare(pe2) < 0;
170 QDebug operator<<(QDebug d, const ProjectEntry &e)
172 d.nospace() << e.directory << ' ' << e.fileName << ' ' << e.type;
176 // --------- ProjectWizardContext
177 struct ProjectWizardContext
179 ProjectWizardContext();
182 QList<Core::IVersionControl*> versionControls;
183 QList<Core::IVersionControl*> activeVersionControls;
184 QList<ProjectEntry> projects;
185 ProjectWizardPage *page;
186 bool repositoryExists; // Is VCS 'add' sufficient, or should a repository be created?
187 QString commonDirectory;
188 const Core::IWizard *wizard;
191 ProjectWizardContext::ProjectWizardContext() :
193 repositoryExists(false),
198 void ProjectWizardContext::clear()
200 activeVersionControls.clear();
202 commonDirectory.clear();
204 repositoryExists = false;
208 // ---- ProjectFileWizardExtension
209 ProjectFileWizardExtension::ProjectFileWizardExtension()
214 ProjectFileWizardExtension::~ProjectFileWizardExtension()
219 static QList<ProjectEntry> findDeployProject(const QList<ProjectEntry> &projects,
222 QList<ProjectEntry> filtered;
223 foreach (const ProjectEntry &project, projects)
224 if (project.node->deploysFolder(commonPath))
229 // Find the project the new files should be added to given their common
230 // path. Either a direct match on the directory or the directory with
231 // the longest matching path (list containing"/project/subproject1" matching
232 // common path "/project/subproject1/newuserpath").
233 static int findMatchingProject(const QList<ProjectEntry> &projects,
234 const QString &commonPath)
236 if (projects.isEmpty() || commonPath.isEmpty())
240 int bestMatchLength = 0;
241 bool bestMatchIsProFile = false;
242 const int count = projects.size();
243 for (int p = 0; p < count; p++) {
244 // Direct match or better match? (note that the wizards' files are native).
245 const ProjectEntry &entry = projects.at(p);
246 const QString &projectDirectory = entry.directory;
247 const int projectDirectorySize = projectDirectory.size();
248 if (projectDirectorySize == bestMatchLength && bestMatchIsProFile)
249 continue; // prefer first pro file over all other files with same bestMatchLength
250 if (projectDirectorySize == bestMatchLength && entry.type == ProjectEntry::PriFile)
251 continue; // we already have a match with same bestMatchLength that is at least a pri file
252 if (projectDirectorySize >= bestMatchLength
253 && commonPath.startsWith(projectDirectory)) {
254 bestMatchIsProFile = (entry.type == ProjectEntry::ProFile);
255 bestMatchLength = projectDirectory.size();
262 static QString generatedProjectFilePath(const QList<Core::GeneratedFile> &files)
264 foreach (const Core::GeneratedFile &file, files)
265 if (file.attributes() & Core::GeneratedFile::OpenProjectAttribute)
270 void ProjectFileWizardExtension::firstExtensionPageShown(
271 const QList<Core::GeneratedFile> &files)
273 initProjectChoices(generatedProjectFilePath(files));
276 qDebug() << Q_FUNC_INFO << files.size();
277 // Parametrize wizard page: find best project to add to, set up files display and
278 // version control depending on path
279 QStringList fileNames;
280 foreach (const Core::GeneratedFile &f, files)
281 fileNames.push_back(f.path());
282 m_context->commonDirectory = Utils::commonPath(fileNames);
283 m_context->page->setFilesDisplay(m_context->commonDirectory, fileNames);
284 // Find best project (Entry at 0 is 'None').
286 int bestProjectIndex = -1;
288 QList<ProjectEntry> deployingProjects = findDeployProject(m_context->projects, m_context->commonDirectory);
289 if (!deployingProjects.isEmpty()) {
290 // Oh we do have someone that deploys it
291 // then the best match is NONE
292 // We display a label explaining that and rename <None> to
294 m_context->page->setNoneLabel(tr("<Implicitly Add>"));
296 QString text = tr("The files are implicitly added to the projects:\n");
297 foreach (const ProjectEntry &project, deployingProjects) {
298 text += project.fileName;
299 text += QLatin1Char('\n');
302 m_context->page->setAdditionalInfo(text);
303 bestProjectIndex = -1;
305 bestProjectIndex = findMatchingProject(m_context->projects, m_context->commonDirectory);
306 m_context->page->setNoneLabel(tr("<None>"));
309 if (bestProjectIndex == -1) {
310 m_context->page->setCurrentProjectIndex(0);
312 m_context->page->setCurrentProjectIndex(bestProjectIndex + 1);
315 // Store all version controls for later use:
316 if (m_context->versionControls.isEmpty()) {
317 foreach (Core::IVersionControl *vc, ExtensionSystem::PluginManager::instance()->getObjects<Core::IVersionControl>()) {
318 m_context->versionControls.append(vc);
319 connect(vc, SIGNAL(configurationChanged()), this, SLOT(initializeVersionControlChoices()));
323 initializeVersionControlChoices();
326 void ProjectFileWizardExtension::initializeVersionControlChoices()
328 // Figure out version control situation:
329 // 1) Directory is managed and VCS supports "Add" -> List it
330 // 2) Directory is managed and VCS does not support "Add" -> None available
331 // 3) Directory is not managed -> Offer all VCS that support "CreateRepository"
333 Core::IVersionControl *currentSelection = 0;
334 int currentIdx = m_context->page->versionControlIndex() - 1;
335 if (currentIdx >= 0 && currentIdx <= m_context->activeVersionControls.size() - 1)
336 currentSelection = m_context->activeVersionControls.at(currentIdx);
338 m_context->activeVersionControls.clear();
340 QStringList versionControlChoices = QStringList(tr("<None>"));
341 if (!m_context->commonDirectory.isEmpty()) {
342 Core::IVersionControl *managingControl = Core::ICore::vcsManager()->findVersionControlForDirectory(m_context->commonDirectory);
343 if (managingControl) {
345 if (managingControl->supportsOperation(Core::IVersionControl::AddOperation)) {
346 versionControlChoices.append(managingControl->displayName());
347 m_context->activeVersionControls.push_back(managingControl);
348 m_context->repositoryExists = true;
352 foreach (Core::IVersionControl *vc, m_context->versionControls)
353 if (vc->supportsOperation(Core::IVersionControl::CreateRepositoryOperation)) {
354 versionControlChoices.append(vc->displayName());
355 m_context->activeVersionControls.append(vc);
357 m_context->repositoryExists = false;
359 } // has a common root.
361 m_context->page->setVersionControls(versionControlChoices);
362 // Enable adding to version control by default.
363 if (m_context->repositoryExists && versionControlChoices.size() >= 2)
364 m_context->page->setVersionControlIndex(1);
365 if (!m_context->repositoryExists) {
366 int newIdx = m_context->activeVersionControls.indexOf(currentSelection) + 1;
367 m_context->page->setVersionControlIndex(newIdx);
371 QList<QWizardPage *> ProjectFileWizardExtension::extensionPages(const Core::IWizard *wizard)
374 m_context = new ProjectWizardContext;
378 // Init context with page and projects
379 m_context->page = new ProjectWizardPage;
380 m_context->wizard = wizard;
381 return QList<QWizardPage *>() << m_context->page;
384 void ProjectFileWizardExtension::initProjectChoices(const QString &generatedProjectFilePath)
386 // Set up project list which remains the same over duration of wizard execution
387 // As tooltip, set the directory for disambiguation (should someone have
388 // duplicate base names in differing directories).
389 //: No project selected
390 QStringList projectChoices(tr("<None>"));
391 QStringList projectToolTips((QString()));
393 typedef QMap<ProjectEntry, bool> ProjectEntryMap;
394 // Sort by base name and purge duplicated entries (resulting from dependencies)
396 ProjectEntryMap entryMap;
398 ProjectNode::ProjectAction projectAction =
399 m_context->wizard->kind() == Core::IWizard::ProjectWizard
400 ? ProjectNode::AddSubProject : ProjectNode::AddNewFile;
401 foreach(ProjectNode *n, AllProjectNodesVisitor::allProjects(projectAction)) {
402 if (projectAction == ProjectNode::AddNewFile
403 || (projectAction == ProjectNode::AddSubProject
404 && n->canAddSubProject(generatedProjectFilePath)))
405 entryMap.insert(ProjectEntry(n), true);
408 m_context->projects.clear();
411 const ProjectEntryMap::const_iterator cend = entryMap.constEnd();
412 for (ProjectEntryMap::const_iterator it = entryMap.constBegin(); it != cend; ++it) {
413 m_context->projects.push_back(it.key());
414 projectChoices.push_back(it.key().fileName);
415 projectToolTips.push_back(QDir::toNativeSeparators(it.key().directory));
418 m_context->page->setProjects(projectChoices);
419 m_context->page->setProjectToolTips(projectToolTips);
420 m_context->page->setAddingSubProject(projectAction == ProjectNode::AddSubProject);
423 bool ProjectFileWizardExtension::processFiles(
424 const QList<Core::GeneratedFile> &files,
425 bool *removeOpenProjectAttribute, QString *errorMessage)
427 return processProject(files, removeOpenProjectAttribute, errorMessage) &&
428 processVersionControl(files, errorMessage);
431 // Add files to project && version control
432 bool ProjectFileWizardExtension::processProject(
433 const QList<Core::GeneratedFile> &files,
434 bool *removeOpenProjectAttribute, QString *errorMessage)
436 typedef QMultiMap<FileType, QString> TypeFileMap;
438 *removeOpenProjectAttribute = false;
440 QString generatedProject = generatedProjectFilePath(files);
442 // Add files to project (Entry at 0 is 'None').
443 const int projectIndex = m_context->page->currentProjectIndex() - 1;
444 if (projectIndex < 0 || projectIndex >= m_context->projects.size())
446 ProjectNode *project = m_context->projects.at(projectIndex).node;
447 if (m_context->wizard->kind() == Core::IWizard::ProjectWizard) {
448 if (!project->addSubProjects(QStringList(generatedProject))) {
449 *errorMessage = tr("Failed to add subproject '%1'\nto project '%2'.")
450 .arg(generatedProject).arg(project->path());
453 *removeOpenProjectAttribute = true;
455 // Split into lists by file type and bulk-add them.
456 TypeFileMap typeFileMap;
457 const Core::MimeDatabase *mdb = Core::ICore::mimeDatabase();
458 foreach (const Core::GeneratedFile &generatedFile, files) {
459 const QString path = generatedFile.path();
460 typeFileMap.insert(typeForFileName(mdb, path), path);
462 foreach (FileType type, typeFileMap.uniqueKeys()) {
463 const QStringList typeFiles = typeFileMap.values(type);
464 if (!project->addFiles(type, typeFiles)) {
465 *errorMessage = tr("Failed to add one or more files to project\n'%1' (%2).").
466 arg(project->path(), typeFiles.join(QString(QLatin1Char(','))));
474 bool ProjectFileWizardExtension::processVersionControl(const QList<Core::GeneratedFile> &files, QString *errorMessage)
476 // Add files to version control (Entry at 0 is 'None').
477 const int vcsIndex = m_context->page->versionControlIndex() - 1;
478 if (vcsIndex < 0 || vcsIndex >= m_context->activeVersionControls.size())
480 QTC_ASSERT(!m_context->commonDirectory.isEmpty(), return false);
481 Core::IVersionControl *versionControl = m_context->activeVersionControls.at(vcsIndex);
482 // Create repository?
483 if (!m_context->repositoryExists) {
484 QTC_ASSERT(versionControl->supportsOperation(Core::IVersionControl::CreateRepositoryOperation), return false);
485 if (!versionControl->vcsCreateRepository(m_context->commonDirectory)) {
486 *errorMessage = tr("A version control system repository could not be created in '%1'.").arg(m_context->commonDirectory);
490 // Add files if supported.
491 if (versionControl->supportsOperation(Core::IVersionControl::AddOperation)) {
492 foreach (const Core::GeneratedFile &generatedFile, files) {
493 if (!versionControl->vcsAdd(generatedFile.path())) {
494 *errorMessage = tr("Failed to add '%1' to the version control system.").arg(generatedFile.path());
502 static TextEditor::ICodeStylePreferences *codeStylePreferences(ProjectExplorer::Project *project, const QString &languageId)
504 if (languageId.isEmpty())
508 return project->editorConfiguration()->codeStyle(languageId);
510 return TextEditor::TextEditorSettings::instance()->codeStyle(languageId);
513 void ProjectFileWizardExtension::applyCodeStyle(Core::GeneratedFile *file) const
515 if (file->isBinary() || file->contents().isEmpty())
516 return; // nothing to do
518 const Core::MimeDatabase *mdb = Core::ICore::mimeDatabase();
519 Core::MimeType mt = mdb->findByFile(QFileInfo(file->path()));
520 const QString languageId = TextEditor::TextEditorSettings::instance()->languageId(mt.type());
522 if (languageId.isEmpty())
523 return; // don't modify files like *.ui *.pro
525 ProjectNode *project = 0;
526 const int projectIndex = m_context->page->currentProjectIndex() - 1;
527 if (projectIndex >= 0 && projectIndex < m_context->projects.size())
528 project = m_context->projects.at(projectIndex).node;
530 ProjectExplorer::Project *baseProject
531 = ProjectExplorer::ProjectExplorerPlugin::instance()->session()->projectForNode(project);
533 TextEditor::ICodeStylePreferencesFactory *factory
534 = TextEditor::TextEditorSettings::instance()->codeStyleFactory(languageId);
536 TextEditor::Indenter *indenter = 0;
538 indenter = factory->createIndenter();
540 indenter = new TextEditor::NormalIndenter();
542 TextEditor::ICodeStylePreferences *codeStylePrefs = codeStylePreferences(baseProject, languageId);
543 indenter->setCodeStylePreferences(codeStylePrefs);
545 QTextDocument doc(file->contents());
546 QTextCursor cursor(&doc);
547 cursor.select(QTextCursor::Document);
548 indenter->indent(&doc, cursor, QChar::Null, codeStylePrefs->currentTabSettings());
549 file->setContents(doc.toPlainText());
553 } // namespace Internal
554 } // namespace ProjectExplorer