From a99e5af1da276ddca391fb56a1a9a5334794f298 Mon Sep 17 00:00:00 2001 From: David Schulz Date: Fri, 5 Jul 2019 09:40:31 +0200 Subject: [PATCH] Python: make interpreter configurable Add settings page to setup python interpreter and use a combo box in the run configuration to switch between those configured interpreter. Change-Id: I51014b785ea822ee138d39c788149f1d22901e55 Reviewed-by: Christian Stenger Reviewed-by: David Schulz --- src/plugins/python/CMakeLists.txt | 2 + .../python/images/settingscategory_python.png | Bin 0 -> 227 bytes .../images/settingscategory_python@2x.png | Bin 0 -> 446 bytes src/plugins/python/python.pro | 7 +- src/plugins/python/python.qbs | 2 + src/plugins/python/python.qrc | 6 + src/plugins/python/pythonconstants.h | 3 + src/plugins/python/pythonplugin.cpp | 124 ++++- src/plugins/python/pythonsettings.cpp | 487 ++++++++++++++++++ src/plugins/python/pythonsettings.h | 62 +++ src/tools/icons/qtcreatoricons.svg | 45 ++ 11 files changed, 722 insertions(+), 16 deletions(-) create mode 100644 src/plugins/python/images/settingscategory_python.png create mode 100644 src/plugins/python/images/settingscategory_python@2x.png create mode 100644 src/plugins/python/python.qrc create mode 100644 src/plugins/python/pythonsettings.cpp create mode 100644 src/plugins/python/pythonsettings.h diff --git a/src/plugins/python/CMakeLists.txt b/src/plugins/python/CMakeLists.txt index 4036e919b38..7dd1f2fab5d 100644 --- a/src/plugins/python/CMakeLists.txt +++ b/src/plugins/python/CMakeLists.txt @@ -1,11 +1,13 @@ add_qtc_plugin(Python PLUGIN_DEPENDS Core QtSupport ProjectExplorer TextEditor SOURCES + python.qrc pythoneditor.cpp pythoneditor.h pythonconstants.h pythonplugin.cpp pythonplugin.h pythonformattoken.h pythonhighlighter.cpp pythonhighlighter.h pythonindenter.cpp pythonindenter.h + pythonsettings.cpp pythonsettings.h pythonscanner.cpp pythonscanner.h ) diff --git a/src/plugins/python/images/settingscategory_python.png b/src/plugins/python/images/settingscategory_python.png new file mode 100644 index 0000000000000000000000000000000000000000..36ad4c2c3c86b94c4e0740125d0770d8cb49f96e GIT binary patch literal 227 zcmeAS@N?(olHy`uVBq!ia0y~yV2}V|4h9AWhNCh`Dhvz^t2|vCLp07O|LA8nxDfm( zag)jHk~hvioOjR1@bK_x%@E?5!=un}m(QnDc3VusS00`JJKvNks(dprbZGolzN|9n z_d}tufJCvcbEQrH=WlED=3r!Xz5e&d|NqCWF8=&KpKI}p8#{vMwlfC@g zKh~8A=Dd8J`iR5&-}^0(f1Up%pTocXZ=OlL!GfQhkxVyVb8ueuJpZp!F8J`2ia83W g6=#2#F)xstq4&pC$#1(gYeBB}boFyt=akR{0Ibbp7XSbN literal 0 HcmV?d00001 diff --git a/src/plugins/python/images/settingscategory_python@2x.png b/src/plugins/python/images/settingscategory_python@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..84f7819d8618e7185eb0bbff8583d3443db549db GIT binary patch literal 446 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4h9AWhN8@6(F_cXt)4E9As)w*fA}jUNL)3@ zF}agla_Pa-s|8tmyr;C;yLJ`v2pov{($2vfT6BN^X{Sh?_e`jGBGeX zap`HjRYNfAe> zPo@$Ue2G~0qQ~db9n(f*D`YuTo`{axlhZe;>`1W6y$LRn6|C`fZ9KDdx`>^il zlH33P&oDm1tSWAJBQaw_!|E&1Um4YY7*A__$uq(3q(W=7^Bw;g7urLsczDh#Tocui z%31L0?EgRer!HXNHryt+;Ykf!^_+S4KK&Q7pK@|-a0$Q8_uCBJ%k4XzL-zicKR@9g zo2u!bp5tO`Hgx>{A3yJ*?xa`i&6j5z704W$Wimxk>W1V6;hm>EV>Ny3Yns#CW>;xvX + + images/settingscategory_python.png + images/settingscategory_python@2x.png + + diff --git a/src/plugins/python/pythonconstants.h b/src/plugins/python/pythonconstants.h index fbea15eed49..0fba0603e92 100644 --- a/src/plugins/python/pythonconstants.h +++ b/src/plugins/python/pythonconstants.h @@ -34,6 +34,9 @@ const char C_PYTHONEDITOR_ID[] = "PythonEditor.PythonEditor"; const char C_EDITOR_DISPLAY_NAME[] = QT_TRANSLATE_NOOP("OpenWith::Editors", "Python Editor"); +const char C_PYTHONOPTIONS_PAGE_ID[] = "PythonEditor.OptionsPage"; +const char C_PYTHON_SETTINGS_CATEGORY[] = "P.Python"; + /******************************************************************************* * MIME type ******************************************************************************/ diff --git a/src/plugins/python/pythonplugin.cpp b/src/plugins/python/pythonplugin.cpp index 93635d7dc77..3dff835fc9b 100644 --- a/src/plugins/python/pythonplugin.cpp +++ b/src/plugins/python/pythonplugin.cpp @@ -27,6 +27,7 @@ #include "pythoneditor.h" #include "pythonconstants.h" #include "pythonhighlighter.h" +#include "pythonsettings.h" #include #include @@ -56,16 +57,19 @@ #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; @@ -217,14 +221,103 @@ private: //////////////////////////////////////////////////////////////// -class InterpreterAspect : public BaseStringAspect +class InterpreterAspect : public ProjectConfigurationAspect { Q_OBJECT public: InterpreterAspect() = default; + + Interpreter currentInterpreter() const; + void updateInterpreters(const QList &interpreters); + void setDefaultInterpreter(const Interpreter &interpreter) { m_defaultId = interpreter.id; } + + void fromMap(const QVariantMap &) override; + void toMap(QVariantMap &) const override; + void addToConfigurationLayout(QFormLayout *layout) override; + +private: + void updateCurrentInterpreter(); + void updateComboBox(); + QList m_interpreters; + QPointer m_comboBox; + QString m_defaultId; + QString m_currentId; }; +Interpreter InterpreterAspect::currentInterpreter() const +{ + return m_comboBox ? m_interpreters.value(m_comboBox->currentIndex()) : Interpreter(); +} + +void InterpreterAspect::updateInterpreters(const QList &interpreters) +{ + m_interpreters = interpreters; + if (m_comboBox) + updateComboBox(); +} + +void InterpreterAspect::fromMap(const QVariantMap &map) +{ + m_currentId = map.value(settingsKey(), m_defaultId).toString(); +} + +void InterpreterAspect::toMap(QVariantMap &map) const +{ + map.insert(settingsKey(), m_currentId); +} + +void InterpreterAspect::addToConfigurationLayout(QFormLayout *layout) +{ + if (QTC_GUARD(m_comboBox.isNull())) + m_comboBox = new QComboBox; + + updateComboBox(); + connect(m_comboBox, + &QComboBox::currentTextChanged, + this, + &InterpreterAspect::updateCurrentInterpreter); + + auto manageButton = new QPushButton(tr("Manage...")); + connect(manageButton, &QPushButton::clicked, []() { + Core::ICore::showOptionsDialog(Constants::C_PYTHONOPTIONS_PAGE_ID); + }); + + auto rowLayout = new QHBoxLayout; + rowLayout->addWidget(m_comboBox); + rowLayout->addWidget(manageButton); + layout->addRow(tr("Interpreter"), rowLayout); +} + +void InterpreterAspect::updateCurrentInterpreter() +{ + m_currentId = currentInterpreter().id; + m_comboBox->setToolTip(currentInterpreter().command.toUserOutput()); + emit changed(); +} + +void InterpreterAspect::updateComboBox() +{ + int currentIndex = -1; + int defaultIndex = -1; + const QString currentId = m_currentId; + m_comboBox->clear(); + for (const Interpreter &interpreter : m_interpreters) { + int index = m_comboBox->count(); + m_comboBox->addItem(interpreter.name); + m_comboBox->setItemData(index, interpreter.command.toUserOutput(), Qt::ToolTipRole); + if (interpreter.id == currentId) + currentIndex = index; + if (interpreter.id == m_defaultId) + defaultIndex = index; + } + if (currentIndex >= 0) + m_comboBox->setCurrentIndex(currentIndex); + else if (defaultIndex >= 0) + m_comboBox->setCurrentIndex(defaultIndex); + updateCurrentInterpreter(); +} + class MainScriptAspect : public BaseStringAspect { Q_OBJECT @@ -251,7 +344,7 @@ private: bool supportsDebugger() const { return true; } QString mainScript() const { return aspect()->value(); } QString arguments() const { return aspect()->arguments(macroExpander()); } - QString interpreter() const { return aspect()->value(); } + QString interpreter() const { return aspect()->currentInterpreter().command.toString(); } void updateTargetInformation(); }; @@ -259,15 +352,14 @@ private: PythonRunConfiguration::PythonRunConfiguration(Target *target, Core::Id id) : RunConfiguration(target, id) { - const Environment sysEnv = Environment::systemEnvironment(); - const QString exec = sysEnv.searchInPath("python").toString(); - auto interpreterAspect = addAspect(); interpreterAspect->setSettingsKey("PythonEditor.RunConfiguation.Interpreter"); - interpreterAspect->setLabelText(tr("Interpreter:")); - interpreterAspect->setDisplayStyle(BaseStringAspect::PathChooserDisplay); - interpreterAspect->setHistoryCompleter("PythonEditor.Interpreter.History"); - interpreterAspect->setValue(exec.isEmpty() ? "python" : exec); + + connect(PythonSettings::instance(), &PythonSettings::interpretersChanged, + interpreterAspect, &InterpreterAspect::updateInterpreters); + + interpreterAspect->updateInterpreters(PythonSettings::interpreters()); + interpreterAspect->setDefaultInterpreter(PythonSettings::defaultInterpreter()); auto scriptAspect = addAspect(); scriptAspect->setSettingsKey("PythonEditor.RunConfiguation.Script"); @@ -282,7 +374,7 @@ PythonRunConfiguration::PythonRunConfiguration(Target *target, Core::Id id) setOutputFormatter(); setCommandLineGetter([this, interpreterAspect, argumentsAspect] { - CommandLine cmd{interpreterAspect->value(), {mainScript()}}; + CommandLine cmd{interpreterAspect->currentInterpreter().command, {mainScript()}}; cmd.addArgs(argumentsAspect->arguments(macroExpander()), CommandLine::Raw); return cmd; }); @@ -737,6 +829,8 @@ bool PythonPlugin::initialize(const QStringList &arguments, QString *errorMessag ProjectManager::registerProjectType(PythonMimeType); + PythonSettings::init(); + return true; } diff --git a/src/plugins/python/pythonsettings.cpp b/src/plugins/python/pythonsettings.cpp new file mode 100644 index 00000000000..2f7b7943811 --- /dev/null +++ b/src/plugins/python/pythonsettings.cpp @@ -0,0 +1,487 @@ +/**************************************************************************** +** +** 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 "pythonsettings.h" + +#include "pythonconstants.h" + +#include +#include "coreplugin/icore.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Python { +namespace Internal { + +using namespace Utils; + +class InterpreterDetailsWidget : public QWidget +{ +public: + InterpreterDetailsWidget() + : m_name(new QLineEdit) + , m_executable(new Utils::PathChooser()) + { + auto mainLayout = new QGridLayout(); + mainLayout->addWidget(new QLabel(tr("Name:")), 0, 0); + mainLayout->addWidget(m_name, 0, 1); + mainLayout->addWidget(new QLabel(tr("Executable")), 1, 0); + mainLayout->addWidget(m_executable, 1, 1); + m_executable->setExpectedKind(Utils::PathChooser::ExistingCommand); + setLayout(mainLayout); + } + + void updateInterpreter(const Interpreter &interpreter) + { + m_name->setText(interpreter.name); + m_executable->setPath(interpreter.command.toString()); + m_currentId = interpreter.id; + } + + Interpreter toInterpreter() + { + return {m_currentId, m_name->text(), FilePath::fromUserInput(m_executable->path())}; + } + QLineEdit *m_name = nullptr; + PathChooser *m_executable = nullptr; + QString m_currentId; +}; + + +class InterpreterOptionsWidget : public QWidget +{ +public: + InterpreterOptionsWidget(const QList &interpreters, + const QString &defaultInterpreter); + + void apply(); + +private: + QTreeView m_view; + ListModel m_model; + InterpreterDetailsWidget *m_detailsWidget = nullptr; + QPushButton *m_deleteButton = nullptr; + QPushButton *m_makeDefaultButton = nullptr; + QString m_defaultId; + + void currentChanged(const QModelIndex &index, const QModelIndex &previous); + void addItem(); + void deleteItem(); + void makeDefault(); +}; + +InterpreterOptionsWidget::InterpreterOptionsWidget(const QList &interpreters, const QString &defaultInterpreter) + : m_detailsWidget(new InterpreterDetailsWidget()) + , m_defaultId(defaultInterpreter) +{ + m_model.setDataAccessor([this](const Interpreter &interpreter, int, int role) -> QVariant { + if (role == Qt::DisplayRole) + return interpreter.name; + if (role == Qt::FontRole) { + QFont f = font(); + f.setBold(interpreter.id == m_defaultId); + return f; + } + return {}; + }); + + for (const Interpreter &interpreter : interpreters) + m_model.appendItem(interpreter); + + auto mainLayout = new QVBoxLayout(); + auto layout = new QHBoxLayout(); + m_view.setModel(&m_model); + m_view.setHeaderHidden(true); + m_view.setSelectionMode(QAbstractItemView::SingleSelection); + m_view.setSelectionBehavior(QAbstractItemView::SelectItems); + connect(m_view.selectionModel(), + &QItemSelectionModel::currentChanged, + this, + &InterpreterOptionsWidget::currentChanged); + auto buttonLayout = new QVBoxLayout(); + auto addButton = new QPushButton(InterpreterOptionsWidget::tr("&Add")); + connect(addButton, &QPushButton::pressed, this, &InterpreterOptionsWidget::addItem); + m_deleteButton = new QPushButton(InterpreterOptionsWidget::tr("&Delete")); + m_deleteButton->setEnabled(false); + connect(m_deleteButton, &QPushButton::pressed, this, &InterpreterOptionsWidget::deleteItem); + m_makeDefaultButton = new QPushButton(InterpreterOptionsWidget::tr("&Make Default")); + m_makeDefaultButton->setEnabled(false); + connect(m_makeDefaultButton, &QPushButton::pressed, this, &InterpreterOptionsWidget::makeDefault); + mainLayout->addLayout(layout); + mainLayout->addWidget(m_detailsWidget); + m_detailsWidget->hide(); + setLayout(mainLayout); + layout->addWidget(&m_view); + layout->addLayout(buttonLayout); + buttonLayout->addWidget(addButton); + buttonLayout->addWidget(m_deleteButton); + buttonLayout->addWidget(m_makeDefaultButton); + buttonLayout->addStretch(10); +} + +void InterpreterOptionsWidget::apply() +{ + const QModelIndex &index = m_view.currentIndex(); + if (index.isValid()) { + m_model.itemAt(index.row())->itemData = m_detailsWidget->toInterpreter(); + emit m_model.dataChanged(index, index); + } + + QList interpreters; + for (const ListItem *treeItem : m_model) + interpreters << treeItem->itemData; + PythonSettings::setInterpreter(interpreters, m_defaultId); +} + +void InterpreterOptionsWidget::currentChanged(const QModelIndex &index, const QModelIndex &previous) +{ + if (previous.isValid()) { + m_model.itemAt(previous.row())->itemData = m_detailsWidget->toInterpreter(); + emit m_model.dataChanged(previous, previous); + } + if (index.isValid()) { + m_detailsWidget->updateInterpreter(m_model.itemAt(index.row())->itemData); + m_detailsWidget->show(); + } else { + m_detailsWidget->hide(); + } + m_deleteButton->setEnabled(index.isValid()); + m_makeDefaultButton->setEnabled(index.isValid()); +} + +void InterpreterOptionsWidget::addItem() +{ + const QModelIndex &index = m_model.indexForItem( + m_model.appendItem({QUuid::createUuid().toString(), "Python", FilePath()})); + QTC_ASSERT(index.isValid(), return); + m_view.setCurrentIndex(index); +} + +void InterpreterOptionsWidget::deleteItem() +{ + const QModelIndex &index = m_view.currentIndex(); + if (index.isValid()) + m_model.destroyItem(m_model.itemAt(index.row())); +} + +class InterpreterOptionsPage : public Core::IOptionsPage +{ + Q_OBJECT + +public: + InterpreterOptionsPage(); + + void setInterpreter(const QList &interpreters) { m_interpreters = interpreters; } + QList interpreters() const { return m_interpreters; } + void setDefaultInterpreter(const QString &defaultId) + { m_defaultInterpreterId = defaultId; } + Interpreter defaultInterpreter() const; + + QWidget *widget() override; + void apply() override; + void finish() override; + +private: + QPointer m_widget; + QList m_interpreters; + QString m_defaultInterpreterId; +}; + +InterpreterOptionsPage::InterpreterOptionsPage() +{ + setId(Constants::C_PYTHONOPTIONS_PAGE_ID); + setDisplayName(tr("Interpreters")); + setCategory(Constants::C_PYTHON_SETTINGS_CATEGORY); + setDisplayCategory(tr("Python")); + setCategoryIcon(Utils::Icon({{":/python/images/settingscategory_python.png", + Utils::Theme::PanelTextColorDark}}, Utils::Icon::Tint)); +} + +Interpreter InterpreterOptionsPage::defaultInterpreter() const +{ + if (m_defaultInterpreterId.isEmpty()) + return {}; + return Utils::findOrDefault(m_interpreters, [this](const Interpreter &interpreter) { + return interpreter.id == m_defaultInterpreterId; + }); +} + +QWidget *InterpreterOptionsPage::widget() +{ + if (!m_widget) + m_widget = new InterpreterOptionsWidget(m_interpreters, m_defaultInterpreterId); + return m_widget; +} + +void InterpreterOptionsPage::apply() +{ + if (m_widget) + m_widget->apply(); +} + +void InterpreterOptionsPage::finish() +{ + delete m_widget; + m_widget = nullptr; +} + +static bool alreadyRegistered(const QList &pythons, const FilePath &pythonExecutable) +{ + return Utils::anyOf(pythons, [pythonExecutable](const Interpreter &interpreter) { + return interpreter.command.toFileInfo().canonicalFilePath() + == pythonExecutable.toFileInfo().canonicalFilePath(); + }); +} + +Interpreter interpreterForPythonExecutable(const FilePath &python, + const QString &defaultName, + bool windowedSuffix = false) +{ + SynchronousProcess pythonProcess; + pythonProcess.setProcessChannelMode(QProcess::MergedChannels); + SynchronousProcessResponse response = pythonProcess.runBlocking( + CommandLine(python, {"--version"})); + QString name; + if (response.result == SynchronousProcessResponse::Finished) + name = response.stdOut().trimmed(); + if (name.isEmpty()) + name = defaultName; + if (windowedSuffix) + name += " (Windowed)"; + return Interpreter{QUuid::createUuid().toString(), name, python}; +} + +static InterpreterOptionsPage &interpreterOptionsPage() +{ + static InterpreterOptionsPage page; + return page; +} + +void InterpreterOptionsWidget::makeDefault() +{ + const QModelIndex &index = m_view.currentIndex(); + if (index.isValid()) { + QModelIndex defaultIndex; + if (auto *defaultItem = m_model.findItemByData( + [this](const Interpreter &interpreter) { return interpreter.id == m_defaultId; })) { + defaultIndex = m_model.indexForItem(defaultItem); + } + m_defaultId = m_model.itemAt(index.row())->itemData.id; + emit m_model.dataChanged(index, index, {Qt::FontRole}); + if (defaultIndex.isValid()) + emit m_model.dataChanged(defaultIndex, defaultIndex, {Qt::FontRole}); + } +} + +constexpr char settingsGroupKey[] = "Python"; +constexpr char interpreterKey[] = "Interpeter"; +constexpr char defaultKey[] = "DefaultInterpeter"; + +struct SavedSettings +{ + QList pythons; + QString defaultId; +}; + +static SavedSettings fromSettings(QSettings *settings) +{ + QList pythons; + settings->beginGroup(settingsGroupKey); + const QVariantList interpreters = settings->value(interpreterKey).toList(); + for (const QVariant &interpreterVar : interpreters) { + auto interpreterList = interpreterVar.toList(); + if (interpreterList.size() != 3) + continue; + pythons << Interpreter{interpreterList.value(0).toString(), + interpreterList.value(1).toString(), + FilePath::fromVariant(interpreterList.value(2))}; + } + + const QString defaultId = settings->value(defaultKey).toString(); + + settings->endGroup(); + + return {pythons, defaultId}; +} + +static void toSettings(QSettings *settings, const SavedSettings &savedSettings) +{ + settings->beginGroup(settingsGroupKey); + const QVariantList interpretersVar + = Utils::transform(savedSettings.pythons, [](const Interpreter &interpreter) { + return QVariant({interpreter.id, interpreter.name, interpreter.command.toVariant()}); + }); + settings->setValue(interpreterKey, interpretersVar); + settings->setValue(defaultKey, savedSettings.defaultId); + settings->endGroup(); +} + +static void addPythonsFromRegistry(QList &pythons) +{ + QSettings pythonRegistry("HKEY_LOCAL_MACHINE\\SOFTWARE\\Python\\PythonCore", + QSettings::NativeFormat); + for (const QString &versionGroup : pythonRegistry.childGroups()) { + pythonRegistry.beginGroup(versionGroup); + QString name = pythonRegistry.value("DisplayName").toString(); + QVariant regVal = pythonRegistry.value("InstallPath/ExecutablePath"); + if (regVal.isValid()) { + const FilePath &executable = FilePath::fromUserInput(regVal.toString()); + if (executable.exists() && !alreadyRegistered(pythons, executable)) { + pythons << Interpreter{QUuid::createUuid().toString(), + name, + FilePath::fromUserInput(regVal.toString())}; + } + } + regVal = pythonRegistry.value("InstallPath/WindowedExecutablePath"); + if (regVal.isValid()) { + const FilePath &executable = FilePath::fromUserInput(regVal.toString()); + if (executable.exists() && !alreadyRegistered(pythons, executable)) { + pythons << Interpreter{QUuid::createUuid().toString(), + name + InterpreterOptionsPage::tr(" (Windowed)"), + FilePath::fromUserInput(regVal.toString())}; + } + } + regVal = pythonRegistry.value("InstallPath/."); + if (regVal.isValid()) { + const FilePath &path = FilePath::fromUserInput(regVal.toString()); + const FilePath &python = path.pathAppended(HostOsInfo::withExecutableSuffix("python")); + if (python.exists() && !alreadyRegistered(pythons, python)) + pythons << interpreterForPythonExecutable(python, "Python " + versionGroup); + const FilePath &pythonw = path.pathAppended( + HostOsInfo::withExecutableSuffix("pythonw")); + if (pythonw.exists() && !alreadyRegistered(pythons, pythonw)) + pythons << interpreterForPythonExecutable(pythonw, "Python " + versionGroup, true); + } + pythonRegistry.endGroup(); + } +} + +static void addPythonsFromPath(QList &pythons) +{ + const auto &env = Environment::systemEnvironment(); + + if (HostOsInfo::isWindowsHost()) { + for (const FilePath &executable : env.findAllInPath("python")) { + if (executable.exists() && !alreadyRegistered(pythons, executable)) + pythons << interpreterForPythonExecutable(executable, "Python from Path"); + } + for (const FilePath &executable : env.findAllInPath("pythonw")) { + if (executable.exists() && !alreadyRegistered(pythons, executable)) + pythons << interpreterForPythonExecutable(executable, "Python from Path", true); + } + } else { + const QStringList filters = {"python", + "python[1-9].[0-9]", + "python[1-9].[1-9][0-9]", + "python[1-9]"}; + for (const FilePath &path : env.path()) { + const QDir dir(path.toString()); + for (const QFileInfo &fi : dir.entryInfoList(filters)) { + const FilePath executable = Utils::FilePath::fromFileInfo(fi); + if (executable.exists() && !alreadyRegistered(pythons, executable)) + pythons << interpreterForPythonExecutable(executable, "Python from Path"); + } + } + } +} + +static QString idForPythonFromPath(QList pythons) +{ + const FilePath &pythonFromPath = Environment::systemEnvironment().searchInPath("python"); + if (pythonFromPath.isEmpty()) + return {}; + const Interpreter &defaultInterpreter + = findOrDefault(pythons, [pythonFromPath](const Interpreter &interpreter) { + return interpreter.command == pythonFromPath; + }); + return defaultInterpreter.id; +} + +static PythonSettings *settingsInstance = nullptr; +PythonSettings::PythonSettings() = default; + +void PythonSettings::init() +{ + QTC_ASSERT(!settingsInstance, return ); + settingsInstance = new PythonSettings(); + + const SavedSettings &settings = fromSettings(Core::ICore::settings()); + QList pythons = settings.pythons; + + if (HostOsInfo::isWindowsHost()) + addPythonsFromRegistry(pythons); + addPythonsFromPath(pythons); + + const QString &defaultId = !settings.defaultId.isEmpty() ? settings.defaultId + : idForPythonFromPath(pythons); + setInterpreter(pythons, defaultId); +} + +void PythonSettings::setInterpreter(const QList &interpreters, const QString &defaultId) +{ + interpreterOptionsPage().setInterpreter(interpreters); + interpreterOptionsPage().setDefaultInterpreter(defaultId); + toSettings(Core::ICore::settings(), {interpreters, defaultId}); + if (QTC_GUARD(settingsInstance)) + emit settingsInstance->interpretersChanged(interpreters, defaultId); +} + +PythonSettings *PythonSettings::instance() +{ + QTC_CHECK(settingsInstance); + return settingsInstance; +} + +QList PythonSettings::interpreters() +{ + return interpreterOptionsPage().interpreters(); +} + +Interpreter PythonSettings::defaultInterpreter() +{ + return interpreterOptionsPage().defaultInterpreter(); +} + +} // namespace Internal +} // namespace Python + +#include "pythonsettings.moc" diff --git a/src/plugins/python/pythonsettings.h b/src/plugins/python/pythonsettings.h new file mode 100644 index 00000000000..583d4fb212f --- /dev/null +++ b/src/plugins/python/pythonsettings.h @@ -0,0 +1,62 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include + +#include + +namespace Python { +namespace Internal { + +class Interpreter +{ +public: + QString id; + QString name; + Utils::FilePath command; +}; + +class PythonSettings : public QObject +{ + Q_OBJECT +public: + static void init(); + + static QList interpreters(); + static Interpreter defaultInterpreter(); + static void setInterpreter(const QList &interpreters, const QString &defaultId); + static PythonSettings *instance(); + +signals: + void interpretersChanged(const QList &interpreters, const QString &defaultId); + +private: + PythonSettings(); +}; + +} // namespace Internal +} // namespace PythonEditor diff --git a/src/tools/icons/qtcreatoricons.svg b/src/tools/icons/qtcreatoricons.svg index e39995f492c..14cddf04208 100644 --- a/src/tools/icons/qtcreatoricons.svg +++ b/src/tools/icons/qtcreatoricons.svg @@ -3217,6 +3217,51 @@ id="path6672" inkscape:connector-curvature="0" /> + + + + + + + +