Files
qt-creator/src/plugins/python/pythonproject.cpp
Eike Ziller 5b364de168 Use dialogParent() instead of mainWindow()
There are very few reasons to use mainWindow() directly.
Especially for modal dialogs, using dialogParent() is important, since
that guarantees the stacking order in case of other dialogs currently
being open.

Change-Id: I7ad2c23c5034b43195eb35cfe405932a7ea003e6
Reviewed-by: hjk <hjk@qt.io>
2020-06-02 11:44:53 +00:00

463 lines
15 KiB
C++

/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "pythonproject.h"
#include "pythonconstants.h"
#include <projectexplorer/buildsystem.h>
#include <projectexplorer/buildtargetinfo.h>
#include <projectexplorer/kitmanager.h>
#include <projectexplorer/projectexplorerconstants.h>
#include <projectexplorer/projectnodes.h>
#include <projectexplorer/target.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QTimer>
#include <coreplugin/documentmanager.h>
#include <coreplugin/icontext.h>
#include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h>
#include <utils/fileutils.h>
using namespace Core;
using namespace ProjectExplorer;
using namespace Utils;
namespace Python {
namespace Internal {
class PythonBuildSystem : public BuildSystem
{
public:
explicit PythonBuildSystem(Target *target);
bool supportsAction(Node *context, ProjectAction action, const Node *node) const override;
bool addFiles(Node *, const QStringList &filePaths, QStringList *) override;
RemovedFilesFromProject removeFiles(Node *, const QStringList &filePaths, QStringList *) override;
bool deleteFiles(Node *, const QStringList &) override;
bool renameFile(Node *, const QString &filePath, const QString &newFilePath) override;
bool saveRawFileList(const QStringList &rawFileList);
bool saveRawList(const QStringList &rawList, const QString &fileName);
void parse();
QStringList processEntries(const QStringList &paths,
QHash<QString, QString> *map = nullptr) const;
bool writePyProjectFile(const QString &fileName, QString &content,
const QStringList &rawList, QString *errorMessage);
void triggerParsing() final;
private:
QStringList m_rawFileList;
QStringList m_files;
QHash<QString, QString> m_rawListEntries;
};
/**
* @brief Provides displayName relative to project node
*/
class PythonFileNode : public FileNode
{
public:
PythonFileNode(const FilePath &filePath, const QString &nodeDisplayName,
FileType fileType = FileType::Source)
: FileNode(filePath, fileType)
, m_displayName(nodeDisplayName)
{}
QString displayName() const override { return m_displayName; }
private:
QString m_displayName;
};
static QStringList readLines(const FilePath &projectFile)
{
const QString projectFileName = projectFile.fileName();
QSet<QString> visited = { projectFileName };
QStringList lines = { projectFileName };
QFile file(projectFile.toString());
if (file.open(QFile::ReadOnly)) {
QTextStream stream(&file);
while (true) {
const QString line = stream.readLine();
if (line.isNull())
break;
if (visited.contains(line))
continue;
lines.append(line);
visited.insert(line);
}
}
return lines;
}
static QStringList readLinesJson(const FilePath &projectFile, QString *errorMessage)
{
const QString projectFileName = projectFile.fileName();
QStringList lines = { projectFileName };
QFile file(projectFile.toString());
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
*errorMessage = PythonProject::tr("Unable to open \"%1\" for reading: %2")
.arg(projectFile.toUserOutput(), file.errorString());
return lines;
}
const QByteArray content = file.readAll();
// This assumes the project file is formed with only one field called
// 'files' that has a list associated of the files to include in the project.
if (content.isEmpty()) {
*errorMessage = PythonProject::tr("Unable to read \"%1\": The file is empty.")
.arg(projectFile.toUserOutput());
return lines;
}
QJsonParseError error;
const QJsonDocument doc = QJsonDocument::fromJson(content, &error);
if (doc.isNull()) {
const int line = content.left(error.offset).count('\n') + 1;
*errorMessage = PythonProject::tr("Unable to parse \"%1\":%2: %3")
.arg(projectFile.toUserOutput()).arg(line)
.arg(error.errorString());
return lines;
}
const QJsonObject obj = doc.object();
if (obj.contains("files")) {
const QJsonValue files = obj.value("files");
const QJsonArray files_array = files.toArray();
QSet<QString> visited;
for (const auto &file : files_array)
visited.insert(file.toString());
lines.append(Utils::toList(visited));
}
return lines;
}
class PythonProjectNode : public ProjectNode
{
public:
PythonProjectNode(const Utils::FilePath &path)
: ProjectNode(path)
{
setDisplayName(path.toFileInfo().completeBaseName());
setAddFileFilter("*.py");
}
};
PythonProject::PythonProject(const FilePath &fileName)
: Project(Constants::C_PY_MIMETYPE, fileName)
{
setId(PythonProjectId);
setProjectLanguages(Context(ProjectExplorer::Constants::CXX_LANGUAGE_ID));
setDisplayName(fileName.toFileInfo().completeBaseName());
setNeedsBuildConfigurations(false);
setBuildSystemCreator([](Target *t) { return new PythonBuildSystem(t); });
}
static FileType getFileType(const FilePath &f)
{
if (f.endsWith(".py"))
return FileType::Source;
if (f.endsWith(".pyproject") || f.endsWith(".pyqtc"))
return FileType::Project;
if (f.endsWith(".qrc"))
return FileType::Resource;
if (f.endsWith(".ui"))
return FileType::Form;
if (f.endsWith(".qml") || f.endsWith(".js"))
return FileType::QML;
return Node::fileTypeForFileName(f);
}
void PythonBuildSystem::triggerParsing()
{
ParseGuard guard = guardParsingRun();
parse();
const QDir baseDir(projectDirectory().toString());
QList<BuildTargetInfo> appTargets;
auto newRoot = std::make_unique<PythonProjectNode>(projectDirectory());
for (const QString &f : qAsConst(m_files)) {
const QString displayName = baseDir.relativeFilePath(f);
const FilePath filePath = FilePath::fromString(f);
const FileType fileType = getFileType(filePath);
newRoot->addNestedNode(std::make_unique<PythonFileNode>(filePath, displayName, fileType));
if (fileType == FileType::Source) {
BuildTargetInfo bti;
bti.buildKey = f;
bti.targetFilePath = filePath;
bti.projectFilePath = projectFilePath();
appTargets.append(bti);
}
}
setRootProjectNode(std::move(newRoot));
setApplicationTargets(appTargets);
guard.markAsSuccess();
emitBuildSystemUpdated();
}
bool PythonBuildSystem::saveRawFileList(const QStringList &rawFileList)
{
const bool result = saveRawList(rawFileList, projectFilePath().toString());
// refresh(PythonProject::Files);
return result;
}
bool PythonBuildSystem::saveRawList(const QStringList &rawList, const QString &fileName)
{
FileChangeBlocker changeGuarg(fileName);
bool result = false;
// New project file
if (fileName.endsWith(".pyproject")) {
FileSaver saver(fileName, QIODevice::ReadOnly | QIODevice::Text);
if (!saver.hasError()) {
QString content = QTextStream(saver.file()).readAll();
if (saver.finalize(ICore::dialogParent())) {
QString errorMessage;
result = writePyProjectFile(fileName, content, rawList, &errorMessage);
if (!errorMessage.isEmpty())
MessageManager::write(errorMessage);
}
}
} else { // Old project file
FileSaver saver(fileName, QIODevice::WriteOnly | QIODevice::Text);
if (!saver.hasError()) {
QTextStream stream(saver.file());
for (const QString &filePath : rawList)
stream << filePath << '\n';
saver.setResult(&stream);
result = saver.finalize(ICore::dialogParent());
}
}
return result;
}
bool PythonBuildSystem::writePyProjectFile(const QString &fileName, QString &content,
const QStringList &rawList, QString *errorMessage)
{
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
*errorMessage = PythonProject::tr("Unable to open \"%1\" for reading: %2")
.arg(fileName, file.errorString());
return false;
}
// Build list of files with the current rawList for the JSON file
QString files("[");
for (const QString &f : rawList)
if (!f.endsWith(".pyproject"))
files += QString("\"%1\",").arg(f);
files = files.left(files.lastIndexOf(',')); // Removing leading comma
files += ']';
// Removing everything inside square parenthesis
// to replace it with the new list of files for the JSON file.
QRegularExpression pattern(R"(\[.*\])");
content.replace(pattern, files);
file.write(content.toUtf8());
return true;
}
bool PythonBuildSystem::addFiles(Node *, const QStringList &filePaths, QStringList *)
{
QStringList newList = m_rawFileList;
const QDir baseDir(projectDirectory().toString());
for (const QString &filePath : filePaths)
newList.append(baseDir.relativeFilePath(filePath));
return saveRawFileList(newList);
}
RemovedFilesFromProject PythonBuildSystem::removeFiles(Node *, const QStringList &filePaths, QStringList *)
{
QStringList newList = m_rawFileList;
for (const QString &filePath : filePaths) {
const QHash<QString, QString>::iterator i = m_rawListEntries.find(filePath);
if (i != m_rawListEntries.end())
newList.removeOne(i.value());
}
bool res = saveRawFileList(newList);
return res ? RemovedFilesFromProject::Ok : RemovedFilesFromProject::Error;
}
bool PythonBuildSystem::deleteFiles(Node *, const QStringList &)
{
return true;
}
bool PythonBuildSystem::renameFile(Node *, const QString &filePath, const QString &newFilePath)
{
QStringList newList = m_rawFileList;
const QHash<QString, QString>::iterator i = m_rawListEntries.find(filePath);
if (i != m_rawListEntries.end()) {
const int index = newList.indexOf(i.value());
if (index != -1) {
const QDir baseDir(projectDirectory().toString());
newList.replace(index, baseDir.relativeFilePath(newFilePath));
}
}
return saveRawFileList(newList);
}
void PythonBuildSystem::parse()
{
m_rawListEntries.clear();
const FilePath filePath = projectFilePath();
// The PySide project file is JSON based
if (filePath.endsWith(".pyproject")) {
QString errorMessage;
m_rawFileList = readLinesJson(filePath, &errorMessage);
if (!errorMessage.isEmpty())
MessageManager::write(errorMessage);
}
// To keep compatibility with PyQt we keep the compatibility with plain
// text files as project files.
else if (filePath.endsWith(".pyqtc"))
m_rawFileList = readLines(filePath);
m_files = processEntries(m_rawFileList, &m_rawListEntries);
}
/**
* Expands environment variables in the given \a string when they are written
* like $$(VARIABLE).
*/
static void expandEnvironmentVariables(const QProcessEnvironment &env, QString &string)
{
static QRegExp candidate(QLatin1String("\\$\\$\\((.+)\\)"));
int index = candidate.indexIn(string);
while (index != -1) {
const QString value = env.value(candidate.cap(1));
string.replace(index, candidate.matchedLength(), value);
index += value.length();
index = candidate.indexIn(string, index);
}
}
/**
* Expands environment variables and converts the path from relative to the
* project to an absolute path.
*
* The \a map variable is an optional argument that will map the returned
* absolute paths back to their original \a paths.
*/
QStringList PythonBuildSystem::processEntries(const QStringList &paths,
QHash<QString, QString> *map) const
{
const QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
const QDir projectDir(projectDirectory().toString());
QFileInfo fileInfo;
QStringList absolutePaths;
for (const QString &path : paths) {
QString trimmedPath = path.trimmed();
if (trimmedPath.isEmpty())
continue;
expandEnvironmentVariables(env, trimmedPath);
trimmedPath = FilePath::fromUserInput(trimmedPath).toString();
fileInfo.setFile(projectDir, trimmedPath);
if (fileInfo.exists()) {
const QString absPath = fileInfo.absoluteFilePath();
absolutePaths.append(absPath);
if (map)
map->insert(absPath, trimmedPath);
}
}
absolutePaths.removeDuplicates();
return absolutePaths;
}
Project::RestoreResult PythonProject::fromMap(const QVariantMap &map, QString *errorMessage)
{
Project::RestoreResult res = Project::fromMap(map, errorMessage);
if (res == RestoreResult::Ok) {
if (!activeTarget())
addTargetForDefaultKit();
}
return res;
}
PythonBuildSystem::PythonBuildSystem(Target *target)
: BuildSystem(target)
{
connect(target->project(), &Project::projectFileIsDirty, this, [this]() { triggerParsing(); });
QTimer::singleShot(0, this, &PythonBuildSystem::triggerParsing);
}
bool PythonBuildSystem::supportsAction(Node *context, ProjectAction action, const Node *node) const
{
if (node->asFileNode()) {
return action == ProjectAction::Rename
|| action == ProjectAction::RemoveFile;
}
if (node->isFolderNodeType() || node->isProjectNodeType()) {
return action == ProjectAction::AddNewFile
|| action == ProjectAction::RemoveFile
|| action == ProjectAction::AddExistingFile;
}
return BuildSystem::supportsAction(context, action, node);
}
} // namespace Internal
} // namespace Python