diff --git a/src/plugins/pythoneditor/PythonEditor.mimetypes.xml b/src/plugins/pythoneditor/PythonEditor.mimetypes.xml
index 23d192286e3..37c97baf11a 100644
--- a/src/plugins/pythoneditor/PythonEditor.mimetypes.xml
+++ b/src/plugins/pythoneditor/PythonEditor.mimetypes.xml
@@ -5,4 +5,9 @@
Python source file without console
+
+
+ Qt Creator Python project file
+
+
diff --git a/src/plugins/pythoneditor/pythoneditorplugin.cpp b/src/plugins/pythoneditor/pythoneditorplugin.cpp
index 881e2a69119..4484ed14a9c 100644
--- a/src/plugins/pythoneditor/pythoneditorplugin.cpp
+++ b/src/plugins/pythoneditor/pythoneditorplugin.cpp
@@ -35,17 +35,38 @@
#include
#include
+#include
#include
#include
#include
+
#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
#include
+
+#include
#include
+#include
+#include
#include
#include
+#include
+using namespace Core;
+using namespace ProjectExplorer;
using namespace PythonEditor::Constants;
+using namespace Utils;
/*******************************************************************************
* List of Python keywords (includes "print" that isn't keyword in python 3
@@ -183,6 +204,1011 @@ static const char *const LIST_OF_PYTHON_BUILTINS[] = {
namespace PythonEditor {
namespace Internal {
+const char PythonRunConfigurationPrefix[] = "PythonEditor.RunConfiguration.";
+const char InterpreterKey[] = "PythonEditor.RunConfiguation.Interpreter";
+const char MainScriptKey[] = "PythonEditor.RunConfiguation.MainScript";
+const char ArgumentsKey[] = "PythonEditor.RunConfiguation.Arguments";
+const char PythonMimeType[] = "text/x-python-project"; // ### FIXME
+const char PythonProjectId[] = "PythonProject";
+const char PythonProjectContext[] = "PythonProjectContext";
+
+class PythonRunConfiguration;
+class PythonProjectFile;
+class PythonProject;
+
+static QString scriptFromId(Core::Id id)
+{
+ return id.suffixAfter(PythonRunConfigurationPrefix);
+}
+
+static Core::Id idFromScript(const QString &target)
+{
+ return Core::Id(PythonRunConfigurationPrefix).withSuffix(target);
+}
+
+class PythonProjectManager : public IProjectManager
+{
+public:
+ PythonProjectManager() {}
+
+ QString mimeType() const { return QLatin1String(PythonMimeType); }
+ Project *openProject(const QString &fileName, QString *errorString);
+
+ void registerProject(PythonProject *project) { m_projects.append(project); }
+ void unregisterProject(PythonProject *project) { m_projects.removeAll(project); }
+
+private:
+ QList m_projects;
+};
+
+class PythonProject : public Project
+{
+public:
+ PythonProject(PythonProjectManager *manager, const QString &filename);
+ ~PythonProject();
+
+ QString displayName() const { return m_projectName; }
+ IDocument *document() const;
+ IProjectManager *projectManager() const { return m_manager; }
+
+ ProjectNode *rootProjectNode() const;
+ QStringList files(FilesMode) const { return m_files; }
+ QStringList files() const { return m_files; }
+
+ bool addFiles(const QStringList &filePaths);
+ bool removeFiles(const QStringList &filePaths);
+ bool setFiles(const QStringList &filePaths);
+ bool renameFile(const QString &filePath, const QString &newFilePath);
+ void refresh();
+
+protected:
+ bool fromMap(const QVariantMap &map);
+
+private:
+ bool saveRawFileList(const QStringList &rawFileList);
+ bool saveRawList(const QStringList &rawList, const QString &fileName);
+ void parseProject();
+ QStringList processEntries(const QStringList &paths,
+ QHash *map = 0) const;
+
+ PythonProjectManager *m_manager;
+ QString m_projectFileName;
+ QString m_projectName;
+ PythonProjectFile *m_document;
+ QStringList m_rawFileList;
+ QStringList m_files;
+ QHash m_rawListEntries;
+
+ ProjectNode *m_rootNode;
+};
+
+class PythonProjectFile : public Core::IDocument
+{
+public:
+ PythonProjectFile(PythonProject *parent, QString fileName)
+ : IDocument(parent),
+ m_project(parent)
+ {
+ setId("Generic.ProjectFile");
+ setMimeType(QLatin1String(PythonMimeType));
+ setFilePath(FileName::fromString(fileName));
+ }
+
+ bool save(QString *errorString, const QString &fileName, bool autoSave)
+ {
+ Q_UNUSED(errorString)
+ Q_UNUSED(fileName)
+ Q_UNUSED(autoSave)
+ return false;
+ }
+
+ QString defaultPath() const { return QString(); }
+ QString suggestedFileName() const { return QString(); }
+
+ bool isModified() const { return false; }
+ bool isSaveAsAllowed() const { return false; }
+
+ ReloadBehavior reloadBehavior(ChangeTrigger state, ChangeType type) const
+ {
+ Q_UNUSED(state)
+ Q_UNUSED(type)
+ return BehaviorSilent;
+ }
+
+ bool reload(QString *errorString, ReloadFlag flag, ChangeType type)
+ {
+ Q_UNUSED(errorString)
+ Q_UNUSED(flag)
+ if (type == TypePermissions)
+ return true;
+ m_project->refresh();
+ return true;
+ }
+
+private:
+ PythonProject *m_project;
+};
+
+class PythonProjectNode : public ProjectNode
+{
+public:
+ PythonProjectNode(PythonProject *project, Core::IDocument *projectFile);
+
+ Core::IDocument *projectFile() const;
+ QString projectFilePath() const;
+
+ bool showInSimpleTree() const;
+
+ QList supportedActions(Node *node) const;
+
+ bool canAddSubProject(const QString &proFilePath) const;
+
+ bool addSubProjects(const QStringList &proFilePaths);
+ bool removeSubProjects(const QStringList &proFilePaths);
+
+ bool addFiles(const QStringList &filePaths, QStringList *notAdded = 0);
+ bool removeFiles(const QStringList &filePaths, QStringList *notRemoved = 0);
+ bool deleteFiles(const QStringList &filePaths);
+ bool renameFile(const QString &filePath, const QString &newFilePath);
+
+ void refresh(QSet oldFileList = QSet());
+
+private:
+ typedef QHash FolderByName;
+ FolderNode *createFolderByName(const QStringList &components, int end);
+ FolderNode *findFolderByName(const QStringList &components, int end);
+ void removeEmptySubFolders(FolderNode *gparent, FolderNode *parent);
+
+private:
+ PythonProject *m_project;
+ Core::IDocument *m_projectFile;
+};
+
+class PythonRunConfigurationWidget : public QWidget
+{
+public:
+ PythonRunConfigurationWidget(PythonRunConfiguration *runConfiguration, QWidget *parent = 0);
+ void setInterpreter(const QString &interpreter);
+
+private:
+ PythonRunConfiguration *m_runConfiguration;
+ DetailsWidget *m_detailsContainer;
+ FancyLineEdit *m_interpreterChooser;
+ QLabel *m_scriptLabel;
+};
+
+class PythonRunConfiguration : public RunConfiguration
+{
+public:
+ PythonRunConfiguration(Target *parent, Core::Id id);
+
+ QWidget *createConfigurationWidget();
+ QVariantMap toMap() const;
+ bool fromMap(const QVariantMap &map);
+ bool isEnabled() const { return m_enabled; }
+ QString disabledReason() const;
+
+ QString mainScript() const { return m_mainScript; }
+ QString interpreter() const { return m_interpreter; }
+ void setInterpreter(const QString &interpreter) { m_interpreter = interpreter; }
+ void setEnabled(bool b);
+
+private:
+ friend class PythonRunConfigurationFactory;
+ PythonRunConfiguration(Target *parent, PythonRunConfiguration *source);
+ QString defaultDisplayName() const;
+
+ QString m_interpreter;
+ QString m_mainScript;
+ bool m_enabled;
+};
+
+class PythonRunControl : public RunControl
+{
+public:
+ PythonRunControl(PythonRunConfiguration *runConfiguration, RunMode mode);
+
+ void start();
+ StopResult stop();
+ bool isRunning() const { return m_running; }
+
+private:
+ void processStarted();
+ void processExited(int exitCode, QProcess::ExitStatus status);
+ void slotAppendMessage(const QString &err, Utils::OutputFormat isError);
+
+ ApplicationLauncher m_applicationLauncher;
+ QString m_interpreter;
+ QString m_mainScript;
+ QString m_commandLineArguments;
+ ApplicationLauncher::Mode m_runMode;
+ bool m_running;
+};
+
+////////////////////////////////////////////////////////////////
+
+PythonRunConfiguration::PythonRunConfiguration(Target *parent, Core::Id id) :
+ RunConfiguration(parent, id),
+ m_mainScript(scriptFromId(id)),
+ m_enabled(true)
+{
+ Environment sysEnv = Environment::systemEnvironment();
+ const QString exec = sysEnv.searchInPath(QLatin1String("python")).toString();
+ m_interpreter = exec.isEmpty() ? QLatin1String("python") : exec;
+
+ addExtraAspect(new LocalEnvironmentAspect(this));
+ addExtraAspect(new ArgumentsAspect(this, QStringLiteral("PythonEditor.RunConfiguration.Arguments")));
+ addExtraAspect(new TerminalAspect(this, QStringLiteral("PythonEditor.RunConfiguration.UseTerminal")));
+ setDefaultDisplayName(defaultDisplayName());
+}
+
+PythonRunConfiguration::PythonRunConfiguration(Target *parent, PythonRunConfiguration *source) :
+ RunConfiguration(parent, source),
+ m_interpreter(source->interpreter()),
+ m_mainScript(source->m_mainScript),
+ m_enabled(source->m_enabled)
+{
+ setDefaultDisplayName(defaultDisplayName());
+}
+
+QVariantMap PythonRunConfiguration::toMap() const
+{
+ QVariantMap map(RunConfiguration::toMap());
+ map.insert(QLatin1String(MainScriptKey), m_mainScript);
+ map.insert(QLatin1String(InterpreterKey), m_interpreter);
+ return map;
+}
+
+bool PythonRunConfiguration::fromMap(const QVariantMap &map)
+{
+ m_mainScript = map.value(QLatin1String(MainScriptKey)).toString();
+ m_interpreter = map.value(QLatin1String(InterpreterKey)).toString();
+ return RunConfiguration::fromMap(map);
+}
+
+QString PythonRunConfiguration::defaultDisplayName() const
+{
+ QString result = tr("Run %1").arg(m_mainScript);
+ if (!m_enabled) {
+ result += QLatin1Char(' ');
+ result += tr("(disabled)");
+ }
+ return result;
+}
+
+QWidget *PythonRunConfiguration::createConfigurationWidget()
+{
+ return new PythonRunConfigurationWidget(this);
+}
+
+void PythonRunConfiguration::setEnabled(bool b)
+{
+ if (m_enabled == b)
+ return;
+ m_enabled = b;
+ emit enabledChanged();
+ setDefaultDisplayName(defaultDisplayName());
+}
+
+QString PythonRunConfiguration::disabledReason() const
+{
+ if (!m_enabled)
+ return tr("The script is currently disabled.");
+ return QString();
+}
+
+PythonRunConfigurationWidget::PythonRunConfigurationWidget(PythonRunConfiguration *runConfiguration, QWidget *parent)
+ : QWidget(parent), m_runConfiguration(runConfiguration)
+{
+ auto fl = new QFormLayout();
+ fl->setMargin(0);
+ fl->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
+
+ m_interpreterChooser = new FancyLineEdit(this);
+ m_interpreterChooser->setText(runConfiguration->interpreter());
+ connect(m_interpreterChooser, &QLineEdit::textChanged,
+ this, &PythonRunConfigurationWidget::setInterpreter);
+
+ m_scriptLabel = new QLabel(this);
+ m_scriptLabel->setText(runConfiguration->mainScript());
+
+ fl->addRow(tr("Interpreter: "), m_interpreterChooser);
+ fl->addRow(tr("Script: "), m_scriptLabel);
+ runConfiguration->extraAspect()->addToMainConfigurationWidget(this, fl);
+ runConfiguration->extraAspect()->addToMainConfigurationWidget(this, fl);
+
+ m_detailsContainer = new DetailsWidget(this);
+ m_detailsContainer->setState(DetailsWidget::NoSummary);
+
+ auto details = new QWidget(m_detailsContainer);
+ m_detailsContainer->setWidget(details);
+ details->setLayout(fl);
+
+ auto vbx = new QVBoxLayout(this);
+ vbx->setMargin(0);
+ vbx->addWidget(m_detailsContainer);
+
+ setEnabled(runConfiguration->isEnabled());
+}
+
+Project *PythonProjectManager::openProject(const QString &fileName, QString *errorString)
+{
+ if (!QFileInfo(fileName).isFile()) {
+ if (errorString)
+ *errorString = tr("Failed opening project \"%1\": Project is not a file.")
+ .arg(fileName);
+ return 0;
+ }
+
+ return new PythonProject(this, fileName);
+}
+
+class PythonRunConfigurationFactory : public IRunConfigurationFactory
+{
+public:
+ PythonRunConfigurationFactory()
+ {
+ setObjectName(QLatin1String("PythonRunConfigurationFactory"));
+ }
+
+ QList availableCreationIds(Target *parent, CreationMode mode) const
+ {
+ Q_UNUSED(mode);
+ if (!canHandle(parent))
+ return {};
+ //return { Core::Id(PythonExecutableId) };
+
+ PythonProject *project = static_cast(parent->project());
+ QList allIds;
+ foreach (const QString &file, project->files())
+ allIds.append(idFromScript(file));
+ return allIds;
+ }
+
+ QString displayNameForId(Core::Id id) const
+ {
+ return scriptFromId(id);
+ }
+
+ bool canCreate(Target *parent, Core::Id id) const
+ {
+ if (!canHandle(parent))
+ return false;
+ PythonProject *project = static_cast(parent->project());
+ return project->files().contains(scriptFromId(id));
+ }
+
+ bool canRestore(Target *parent, const QVariantMap &map) const
+ {
+ Q_UNUSED(parent);
+ return idFromMap(map).name().startsWith(PythonRunConfigurationPrefix);
+ }
+
+ bool canClone(Target *parent, RunConfiguration *source) const
+ {
+ if (!canHandle(parent))
+ return false;
+ return source->id().name().startsWith(PythonRunConfigurationPrefix);
+ }
+
+ RunConfiguration *clone(Target *parent, RunConfiguration *source)
+ {
+ if (!canClone(parent, source))
+ return 0;
+ return new PythonRunConfiguration(parent, static_cast(source));
+ }
+
+private:
+ bool canHandle(Target *parent) const { return dynamic_cast(parent->project()); }
+
+ RunConfiguration *doCreate(Target *parent, Core::Id id)
+ {
+ return new PythonRunConfiguration(parent, id);
+ }
+
+ RunConfiguration *doRestore(Target *parent, const QVariantMap &map)
+ {
+ Core::Id id(idFromMap(map));
+ return new PythonRunConfiguration(parent, id);
+ }
+};
+
+
+PythonProject::PythonProject(PythonProjectManager *manager, const QString &fileName)
+ : m_manager(manager),
+ m_projectFileName(fileName)
+{
+ setId(PythonProjectId);
+ setProjectContext(Context(PythonProjectContext));
+ setProjectLanguages(Context(ProjectExplorer::Constants::LANG_CXX));
+
+ QFileInfo fileInfo(m_projectFileName);
+
+ m_projectName = fileInfo.completeBaseName();
+ m_document = new PythonProjectFile(this, m_projectFileName);
+
+ DocumentManager::addDocument(m_document);
+
+ m_rootNode = new PythonProjectNode(this, m_document);
+
+ m_manager->registerProject(this);
+}
+
+PythonProject::~PythonProject()
+{
+ m_manager->unregisterProject(this);
+
+ delete m_rootNode;
+}
+
+IDocument *PythonProject::document() const
+{
+ return m_document;
+}
+
+static QStringList readLines(const QString &absoluteFileName)
+{
+ QStringList lines;
+
+ QFile file(absoluteFileName);
+ if (file.open(QFile::ReadOnly)) {
+ QTextStream stream(&file);
+
+ forever {
+ QString line = stream.readLine();
+ if (line.isNull())
+ break;
+
+ lines.append(line);
+ }
+ }
+
+ return lines;
+}
+
+bool PythonProject::saveRawFileList(const QStringList &rawFileList)
+{
+ bool result = saveRawList(rawFileList, m_projectFileName);
+// refresh(PythonProject::Files);
+ return result;
+}
+
+bool PythonProject::saveRawList(const QStringList &rawList, const QString &fileName)
+{
+ DocumentManager::expectFileChange(fileName);
+ // Make sure we can open the file for writing
+ FileSaver saver(fileName, QIODevice::Text);
+ if (!saver.hasError()) {
+ QTextStream stream(saver.file());
+ foreach (const QString &filePath, rawList)
+ stream << filePath << QLatin1Char('\n');
+ saver.setResult(&stream);
+ }
+ bool result = saver.finalize(ICore::mainWindow());
+ DocumentManager::unexpectFileChange(fileName);
+ return result;
+}
+
+bool PythonProject::addFiles(const QStringList &filePaths)
+{
+ QStringList newList = m_rawFileList;
+
+ QDir baseDir(QFileInfo(m_projectFileName).dir());
+ foreach (const QString &filePath, filePaths)
+ newList.append(baseDir.relativeFilePath(filePath));
+
+ QSet toAdd;
+
+ foreach (const QString &filePath, filePaths) {
+ QString directory = QFileInfo(filePath).absolutePath();
+ if (!toAdd.contains(directory))
+ toAdd << directory;
+ }
+
+ bool result = saveRawList(newList, m_projectFileName);
+ refresh();
+
+ return result;
+}
+
+bool PythonProject::removeFiles(const QStringList &filePaths)
+{
+ QStringList newList = m_rawFileList;
+
+ foreach (const QString &filePath, filePaths) {
+ QHash::iterator i = m_rawListEntries.find(filePath);
+ if (i != m_rawListEntries.end())
+ newList.removeOne(i.value());
+ }
+
+ return saveRawFileList(newList);
+}
+
+bool PythonProject::setFiles(const QStringList &filePaths)
+{
+ QStringList newList;
+ QDir baseDir(QFileInfo(m_projectFileName).dir());
+ foreach (const QString &filePath, filePaths)
+ newList.append(baseDir.relativeFilePath(filePath));
+
+ return saveRawFileList(newList);
+}
+
+bool PythonProject::renameFile(const QString &filePath, const QString &newFilePath)
+{
+ QStringList newList = m_rawFileList;
+
+ QHash::iterator i = m_rawListEntries.find(filePath);
+ if (i != m_rawListEntries.end()) {
+ int index = newList.indexOf(i.value());
+ if (index != -1) {
+ QDir baseDir(QFileInfo(m_projectFileName).dir());
+ newList.replace(index, baseDir.relativeFilePath(newFilePath));
+ }
+ }
+
+ return saveRawFileList(newList);
+}
+
+void PythonProject::parseProject()
+{
+ m_rawListEntries.clear();
+ m_rawFileList = readLines(m_projectFileName);
+ m_rawFileList << FileName::fromString(m_projectFileName).fileName();
+ m_files = processEntries(m_rawFileList, &m_rawListEntries);
+ emit fileListChanged();
+}
+
+/**
+ * @brief Provides displayName relative to project node
+ */
+class PythonFileNode : public ProjectExplorer::FileNode
+{
+public:
+ PythonFileNode(const Utils::FileName &filePath, const QString &nodeDisplayName)
+ : ProjectExplorer::FileNode(filePath, SourceType, false)
+ , m_displayName(nodeDisplayName)
+ {}
+
+ QString displayName() const { return m_displayName; }
+private:
+ QString m_displayName;
+};
+
+void PythonProject::refresh()
+{
+ m_rootNode->removeFileNodes(m_rootNode->fileNodes());
+ parseProject();
+
+ QDir baseDir = FileName::fromString(m_projectFileName).toFileInfo().absoluteDir();
+
+ QList fileNodes;
+ foreach (const QString &file, m_files) {
+ QString displayName = baseDir.relativeFilePath(file);
+ fileNodes.append(new PythonFileNode(FileName::fromString(file), displayName));
+ }
+
+ m_rootNode->addFileNodes(fileNodes);
+}
+
+/**
+ * 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 entries.
+ */
+QStringList PythonProject::processEntries(const QStringList &paths,
+ QHash *map) const
+{
+ const QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
+ const QDir projectDir(QFileInfo(m_projectFileName).dir());
+
+ QFileInfo fileInfo;
+ QStringList absolutePaths;
+ foreach (const QString &path, paths) {
+ QString trimmedPath = path.trimmed();
+ if (trimmedPath.isEmpty())
+ continue;
+
+ expandEnvironmentVariables(env, trimmedPath);
+
+ trimmedPath = FileName::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;
+}
+
+ProjectNode *PythonProject::rootProjectNode() const
+{
+ return m_rootNode;
+}
+
+bool PythonProject::fromMap(const QVariantMap &map)
+{
+ if (!Project::fromMap(map))
+ return false;
+
+ Kit *defaultKit = KitManager::defaultKit();
+ if (!activeTarget() && defaultKit)
+ addTarget(createTarget(defaultKit));
+
+ refresh();
+
+ QList targetList = targets();
+ foreach (Target *t, targetList) {
+ foreach (const QString &file, m_files)
+ t->addRunConfiguration(new PythonRunConfiguration(t, idFromScript(file)));
+ }
+
+ return true;
+}
+
+PythonProjectNode::PythonProjectNode(PythonProject *project, Core::IDocument *projectFile)
+ : ProjectNode(projectFile->filePath())
+ , m_project(project)
+ , m_projectFile(projectFile)
+{
+ setDisplayName(projectFile->filePath().toFileInfo().completeBaseName());
+}
+
+Core::IDocument *PythonProjectNode::projectFile() const
+{
+ return m_projectFile;
+}
+
+QString PythonProjectNode::projectFilePath() const
+{
+ return m_projectFile->filePath().toString();
+}
+
+QHash sortFilesIntoPaths(const QString &base, const QSet &files)
+{
+ QHash filesInPath;
+ const QDir baseDir(base);
+
+ foreach (const QString &absoluteFileName, files) {
+ QFileInfo fileInfo(absoluteFileName);
+ FileName absoluteFilePath = FileName::fromString(fileInfo.path());
+ QString relativeFilePath;
+
+ if (absoluteFilePath.isChildOf(baseDir)) {
+ relativeFilePath = absoluteFilePath.relativeChildPath(FileName::fromString(base)).toString();
+ } else {
+ // 'file' is not part of the project.
+ relativeFilePath = baseDir.relativeFilePath(absoluteFilePath.toString());
+ if (relativeFilePath.endsWith(QLatin1Char('/')))
+ relativeFilePath.chop(1);
+ }
+
+ filesInPath[relativeFilePath].append(absoluteFileName);
+ }
+ return filesInPath;
+}
+
+void PythonProjectNode::refresh(QSet oldFileList)
+{
+ typedef QHash FilesInPathHash;
+ typedef FilesInPathHash::ConstIterator FilesInPathHashConstIt;
+
+ // Do those separately
+ oldFileList.remove(m_project->projectFilePath().toString());
+
+ QSet newFileList = m_project->files().toSet();
+ newFileList.remove(m_project->projectFilePath().toString());
+
+ QSet removed = oldFileList;
+ removed.subtract(newFileList);
+ QSet added = newFileList;
+ added.subtract(oldFileList);
+
+ QString baseDir = path().toFileInfo().absolutePath();
+ FilesInPathHash filesInPaths = sortFilesIntoPaths(baseDir, added);
+
+ FilesInPathHashConstIt cend = filesInPaths.constEnd();
+ for (FilesInPathHashConstIt it = filesInPaths.constBegin(); it != cend; ++it) {
+ const QString &filePath = it.key();
+ QStringList components;
+ if (!filePath.isEmpty())
+ components = filePath.split(QLatin1Char('/'));
+ FolderNode *folder = findFolderByName(components, components.size());
+ if (!folder)
+ folder = createFolderByName(components, components.size());
+
+ QList fileNodes;
+ foreach (const QString &file, it.value()) {
+ FileType fileType = SourceType; // ### FIXME
+ if (file.endsWith(QLatin1String(".qrc")))
+ fileType = ResourceType;
+ FileNode *fileNode = new FileNode(FileName::fromString(file),
+ fileType, /*generated = */ false);
+ fileNodes.append(fileNode);
+ }
+
+ folder->addFileNodes(fileNodes);
+ }
+
+ filesInPaths = sortFilesIntoPaths(baseDir, removed);
+ cend = filesInPaths.constEnd();
+ for (FilesInPathHashConstIt it = filesInPaths.constBegin(); it != cend; ++it) {
+ const QString &filePath = it.key();
+ QStringList components;
+ if (!filePath.isEmpty())
+ components = filePath.split(QLatin1Char('/'));
+ FolderNode *folder = findFolderByName(components, components.size());
+
+ QList fileNodes;
+ foreach (const QString &file, it.value()) {
+ foreach (FileNode *fn, folder->fileNodes()) {
+ if (fn->path().toString() == file)
+ fileNodes.append(fn);
+ }
+ }
+
+ folder->removeFileNodes(fileNodes);
+ }
+
+ foreach (FolderNode *fn, subFolderNodes())
+ removeEmptySubFolders(this, fn);
+
+}
+
+void PythonProjectNode::removeEmptySubFolders(FolderNode *gparent, FolderNode *parent)
+{
+ foreach (FolderNode *fn, parent->subFolderNodes())
+ removeEmptySubFolders(parent, fn);
+
+ if (parent->subFolderNodes().isEmpty() && parent->fileNodes().isEmpty())
+ gparent->removeFolderNodes(QList() << parent);
+}
+
+FolderNode *PythonProjectNode::createFolderByName(const QStringList &components, int end)
+{
+ if (end == 0)
+ return this;
+
+ QString folderName;
+ for (int i = 0; i < end; ++i) {
+ folderName.append(components.at(i));
+ folderName += QLatin1Char('/');
+ }
+
+ const QString component = components.at(end - 1);
+
+ const FileName folderPath = path().parentDir().appendPath(folderName);
+ FolderNode *folder = new FolderNode(folderPath);
+ folder->setDisplayName(component);
+
+ FolderNode *parent = findFolderByName(components, end - 1);
+ if (!parent)
+ parent = createFolderByName(components, end - 1);
+ parent->addFolderNodes(QList() << folder);
+
+ return folder;
+}
+
+FolderNode *PythonProjectNode::findFolderByName(const QStringList &components, int end)
+{
+ if (end == 0)
+ return this;
+
+ QString folderName;
+ for (int i = 0; i < end; ++i) {
+ folderName.append(components.at(i));
+ folderName += QLatin1Char('/');
+ }
+
+ FolderNode *parent = findFolderByName(components, end - 1);
+
+ if (!parent)
+ return 0;
+
+ const QString baseDir = path().toFileInfo().path();
+ foreach (FolderNode *fn, parent->subFolderNodes()) {
+ if (fn->path().toString() == baseDir + QLatin1Char('/') + folderName)
+ return fn;
+ }
+ return 0;
+}
+
+bool PythonProjectNode::showInSimpleTree() const
+{
+ return true;
+}
+
+QList PythonProjectNode::supportedActions(Node *node) const
+{
+ Q_UNUSED(node);
+ //return { AddNewFile, AddExistingFile, AddExistingDirectory, RemoveFile, Rename };
+ return {};
+}
+
+bool PythonProjectNode::canAddSubProject(const QString &proFilePath) const
+{
+ Q_UNUSED(proFilePath)
+ return false;
+}
+
+bool PythonProjectNode::addSubProjects(const QStringList &proFilePaths)
+{
+ Q_UNUSED(proFilePaths)
+ return false;
+}
+
+bool PythonProjectNode::removeSubProjects(const QStringList &proFilePaths)
+{
+ Q_UNUSED(proFilePaths)
+ return false;
+}
+
+bool PythonProjectNode::addFiles(const QStringList &filePaths, QStringList *notAdded)
+{
+ Q_UNUSED(notAdded)
+
+ return m_project->addFiles(filePaths);
+}
+
+bool PythonProjectNode::removeFiles(const QStringList &filePaths, QStringList *notRemoved)
+{
+ Q_UNUSED(notRemoved)
+
+ return m_project->removeFiles(filePaths);
+}
+
+bool PythonProjectNode::deleteFiles(const QStringList &filePaths)
+{
+ Q_UNUSED(filePaths)
+ return false;
+}
+
+bool PythonProjectNode::renameFile(const QString &filePath, const QString &newFilePath)
+{
+ return m_project->renameFile(filePath, newFilePath);
+}
+
+// PythonRunControlFactory
+
+class PythonRunControlFactory : public IRunControlFactory
+{
+public:
+ bool canRun(RunConfiguration *runConfiguration, RunMode mode) const;
+ RunControl *create(RunConfiguration *runConfiguration, RunMode mode, QString *errorMessage);
+};
+
+bool PythonRunControlFactory::canRun(RunConfiguration *runConfiguration, RunMode mode) const
+{
+ return mode == NormalRunMode && dynamic_cast(runConfiguration);
+}
+
+RunControl *PythonRunControlFactory::create(RunConfiguration *runConfiguration, RunMode mode, QString *errorMessage)
+{
+ Q_UNUSED(errorMessage)
+ QTC_ASSERT(canRun(runConfiguration, mode), return 0);
+ return new PythonRunControl(static_cast(runConfiguration), mode);
+}
+
+// PythonRunControl
+
+PythonRunControl::PythonRunControl(PythonRunConfiguration *rc, RunMode mode)
+ : RunControl(rc, mode), m_running(false)
+{
+ setIcon(QLatin1String(ProjectExplorer::Constants::ICON_RUN_SMALL));
+ EnvironmentAspect *environment = rc->extraAspect();
+ Utils::Environment env;
+ if (environment)
+ env = environment->environment();
+ m_applicationLauncher.setEnvironment(env);
+
+ m_interpreter = rc->interpreter();
+ m_mainScript = rc->mainScript();
+ m_runMode = rc->extraAspect()->runMode();
+ m_commandLineArguments = rc->extraAspect()->arguments();
+
+ connect(&m_applicationLauncher, &ApplicationLauncher::appendMessage,
+ this, &PythonRunControl::slotAppendMessage);
+ connect(&m_applicationLauncher, &ApplicationLauncher::processStarted,
+ this, &PythonRunControl::processStarted);
+ connect(&m_applicationLauncher, &ApplicationLauncher::processExited,
+ this, &PythonRunControl::processExited);
+ connect(&m_applicationLauncher, &ApplicationLauncher::bringToForegroundRequested,
+ this, &RunControl::bringApplicationToForeground);
+}
+
+void PythonRunControl::start()
+{
+ emit started();
+ if (m_interpreter.isEmpty()) {
+ appendMessage(tr("No Python interpreter specified.") + QLatin1Char('\n'), Utils::ErrorMessageFormat);
+ emit finished();
+ } else if (!QFileInfo::exists(m_interpreter)) {
+ appendMessage(tr("Python interpreter %1 does not exist.").arg(QDir::toNativeSeparators(m_interpreter)) + QLatin1Char('\n'),
+ Utils::ErrorMessageFormat);
+ emit finished();
+ } else {
+ m_running = true;
+ QString msg = tr("Starting %1...").arg(QDir::toNativeSeparators(m_interpreter)) + QLatin1Char('\n');
+ appendMessage(msg, Utils::NormalMessageFormat);
+
+ QString args;
+ QtcProcess::addArg(&args, m_mainScript);
+ QtcProcess::addArgs(&args, m_commandLineArguments);
+ m_applicationLauncher.start(m_runMode, m_interpreter, args);
+
+ setApplicationProcessHandle(ProcessHandle(m_applicationLauncher.applicationPID()));
+ }
+}
+
+PythonRunControl::StopResult PythonRunControl::stop()
+{
+ m_applicationLauncher.stop();
+ return StoppedSynchronously;
+}
+
+void PythonRunControl::slotAppendMessage(const QString &err, Utils::OutputFormat format)
+{
+ appendMessage(err, format);
+}
+
+void PythonRunControl::processStarted()
+{
+ // Console processes only know their pid after being started
+ setApplicationProcessHandle(ProcessHandle(m_applicationLauncher.applicationPID()));
+}
+
+void PythonRunControl::processExited(int exitCode, QProcess::ExitStatus status)
+{
+ m_running = false;
+ setApplicationProcessHandle(ProcessHandle());
+ QString msg;
+ if (status == QProcess::CrashExit) {
+ msg = tr("%1 crashed")
+ .arg(QDir::toNativeSeparators(m_interpreter));
+ } else {
+ msg = tr("%1 exited with code %2")
+ .arg(QDir::toNativeSeparators(m_interpreter)).arg(exitCode);
+ }
+ appendMessage(msg + QLatin1Char('\n'), Utils::NormalMessageFormat);
+ emit finished();
+}
+
+void PythonRunConfigurationWidget::setInterpreter(const QString &interpreter)
+{
+ m_runConfiguration->setInterpreter(interpreter);
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// PythonEditorPlugin
+//
+////////////////////////////////////////////////////////////////////////////////////
+
static PythonEditorPlugin *m_instance = 0;
/// Copies identifiers from array to QSet
@@ -211,9 +1237,12 @@ bool PythonEditorPlugin::initialize(const QStringList &arguments, QString *error
Q_UNUSED(arguments)
Q_UNUSED(errorMessage)
- Utils::MimeDatabase::addMimeTypes(QLatin1String(":/pythoneditor/PythonEditor.mimetypes.xml"));
+ MimeDatabase::addMimeTypes(QLatin1String(":/pythoneditor/PythonEditor.mimetypes.xml"));
+ addAutoReleasedObject(new PythonProjectManager);
addAutoReleasedObject(new PythonEditorFactory);
+ addAutoReleasedObject(new PythonRunConfigurationFactory);
+ addAutoReleasedObject(new PythonRunControlFactory);
// Initialize editor actions handler
// Add MIME overlay icons (these icons displayed at Project dock panel)