diff --git a/src/plugins/python/CMakeLists.txt b/src/plugins/python/CMakeLists.txt index 53e1a960bcf..dad146f3217 100644 --- a/src/plugins/python/CMakeLists.txt +++ b/src/plugins/python/CMakeLists.txt @@ -3,6 +3,7 @@ add_qtc_plugin(Python PLUGIN_DEPENDS Core LanguageClient ProjectExplorer TextEditor SOURCES pipsupport.cpp pipsupport.h + pyside.cpp pyside.h python.qrc pythonconstants.h pythoneditor.cpp pythoneditor.h diff --git a/src/plugins/python/pyside.cpp b/src/plugins/python/pyside.cpp new file mode 100644 index 00000000000..a943f88b54f --- /dev/null +++ b/src/plugins/python/pyside.cpp @@ -0,0 +1,186 @@ +/**************************************************************************** +** +** Copyright (C) 2022 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 "pyside.h" + +#include "pythonplugin.h" +#include "pythonproject.h" +#include "pythonrunconfiguration.h" +#include "pythonsettings.h" +#include "pythonutils.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace Utils; + +namespace Python { +namespace Internal { + +static constexpr char installPySideInfoBarId[] = "Python::InstallPySide"; + +PySideInstaller *PySideInstaller::instance() +{ + static PySideInstaller *instance = new PySideInstaller; + return instance; +} + +void PySideInstaller::checkPySideInstallation(const Utils::FilePath &python, + TextEditor::TextDocument *document) +{ + document->infoBar()->removeInfo(installPySideInfoBarId); + const QString pySide = importedPySide(document->plainText()); + if (pySide == "PySide2" || pySide == "PySide6") + runPySideChecker(python, pySide, document); +} + +bool PySideInstaller::missingPySideInstallation(const Utils::FilePath &pythonPath, + const QString &pySide) +{ + QTC_ASSERT(!pySide.isEmpty(), return false); + static QMap> pythonWithPyside; + if (pythonWithPyside[pythonPath].contains(pySide)) + return false; + + QtcProcess pythonProcess; + const CommandLine importPySideCheck(pythonPath, {"-c", "import " + pySide}); + pythonProcess.setCommand(importPySideCheck); + pythonProcess.runBlocking(); + const bool missing = pythonProcess.result() != ProcessResult::FinishedWithSuccess; + if (!missing) + pythonWithPyside[pythonPath].insert(pySide); + return missing; +} + +QString PySideInstaller::importedPySide(const QString &text) +{ + static QRegularExpression importScanner("^\\s*(import|from)\\s+(PySide\\d)", + QRegularExpression::MultilineOption); + const QRegularExpressionMatch match = importScanner.match(text); + return match.captured(2); +} + +PySideInstaller::PySideInstaller() + : QObject(PythonPlugin::instance()) +{} + +void PySideInstaller::installPyside(const Utils::FilePath &python, + const QString &pySide, + TextEditor::TextDocument *document) +{ + document->infoBar()->removeInfo(installPySideInfoBarId); + + auto install = new PipInstallTask(python); + connect(install, &PipInstallTask::finished, install, &QObject::deleteLater); + install->setPackage(PipPackage(pySide)); + install->run(); +} + +void PySideInstaller::changeInterpreter(const QString &interpreterId, + PythonRunConfiguration *runConfig) +{ + if (runConfig) + runConfig->setInterpreter(PythonSettings::interpreter(interpreterId)); +} + +void PySideInstaller::handlePySideMissing(const FilePath &python, + const QString &pySide, + TextEditor::TextDocument *document) +{ + if (!document || !document->infoBar()->canInfoBeAdded(installPySideInfoBarId)) + return; + const QString message = tr("%1 installation missing for %2 (%3)") + .arg(pySide, pythonName(python), python.toUserOutput()); + InfoBarEntry info(installPySideInfoBarId, message, InfoBarEntry::GlobalSuppression::Enabled); + auto installCallback = [=]() { installPyside(python, pySide, document); }; + const QString installTooltip = tr("Install %1 for %2 using pip package installer.") + .arg(pySide, python.toUserOutput()); + info.addCustomButton(tr("Install"), installCallback, installTooltip); + + if (PythonProject *project = pythonProjectForFile(document->filePath())) { + if (ProjectExplorer::Target *target = project->activeTarget()) { + if (auto runConfiguration = qobject_cast( + target->activeRunConfiguration())) { + const QList interpreters = Utils::transform( + PythonSettings::interpreters(), [](const Interpreter &interpreter) { + return InfoBarEntry::ComboInfo{interpreter.name, interpreter.id}; + }); + auto interpreterChangeCallback = + [=, rc = QPointer(runConfiguration)]( + const InfoBarEntry::ComboInfo &info) { + changeInterpreter(info.data.toString(), rc); + }; + + const auto isCurrentInterpreter + = Utils::equal(&InfoBarEntry::ComboInfo::data, + QVariant(runConfiguration->interpreter().id)); + const QString switchTooltip = tr("Switch the Python interpreter for %1") + .arg(runConfiguration->displayName()); + info.setComboInfo(interpreters, + interpreterChangeCallback, + switchTooltip, + Utils::indexOf(interpreters, isCurrentInterpreter)); + } + } + } + document->infoBar()->addInfo(info); +} + +void PySideInstaller::runPySideChecker(const Utils::FilePath &python, + const QString &pySide, + TextEditor::TextDocument *document) +{ + using CheckPySideWatcher = QFutureWatcher; + + QPointer watcher = new CheckPySideWatcher(); + + // cancel and delete watcher after a 10 second timeout + QTimer::singleShot(10000, this, [watcher]() { + if (watcher) { + watcher->cancel(); + watcher->deleteLater(); + } + }); + connect(watcher, + &CheckPySideWatcher::resultReadyAt, + this, + [=, document = QPointer(document)]() { + if (watcher->result()) + handlePySideMissing(python, pySide, document); + watcher->deleteLater(); + }); + watcher->setFuture( + Utils::runAsync(&missingPySideInstallation, python, pySide)); +} + +} // namespace Internal +} // namespace Python diff --git a/src/plugins/python/pyside.h b/src/plugins/python/pyside.h new file mode 100644 index 00000000000..32c46719b40 --- /dev/null +++ b/src/plugins/python/pyside.h @@ -0,0 +1,67 @@ +/**************************************************************************** +** +** Copyright (C) 2022 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 "pipsupport.h" + +#include + +#include + +namespace TextEditor { class TextDocument; } +namespace Python { +namespace Internal { + +class PythonRunConfiguration; + +class PySideInstaller : public QObject +{ + Q_DECLARE_TR_FUNCTIONS(Python::Internal::PySideInstaller) +public: + static PySideInstaller *instance(); + void checkPySideInstallation(const Utils::FilePath &python, TextEditor::TextDocument *document); + +private: + PySideInstaller(); + + void installPyside(const Utils::FilePath &python, + const QString &pySide, TextEditor::TextDocument *document); + void changeInterpreter(const QString &interpreterId, PythonRunConfiguration *runConfig); + void handlePySideMissing(const Utils::FilePath &python, + const QString &pySide, + TextEditor::TextDocument *document); + + void runPySideChecker(const Utils::FilePath &python, + const QString &pySide, + TextEditor::TextDocument *document); + static bool missingPySideInstallation(const Utils::FilePath &python, const QString &pySide); + static QString importedPySide(const QString &text); + + QHash> m_infoBarEntries; +}; + +} // namespace Internal +} // namespace Python diff --git a/src/plugins/python/python.qbs b/src/plugins/python/python.qbs index 2003192a318..3f14f992baf 100644 --- a/src/plugins/python/python.qbs +++ b/src/plugins/python/python.qbs @@ -19,6 +19,8 @@ QtcPlugin { files: [ "pipsupport.cpp", "pipsupport.h", + "pyside.cpp", + "pyside.h", "python.qrc", "pythonconstants.h", "pythoneditor.cpp", diff --git a/src/plugins/python/pythoneditor.cpp b/src/plugins/python/pythoneditor.cpp index 264ef809e56..6344e52f9b0 100644 --- a/src/plugins/python/pythoneditor.cpp +++ b/src/plugins/python/pythoneditor.cpp @@ -25,6 +25,7 @@ #include "pythoneditor.h" +#include "pyside.h" #include "pythonconstants.h" #include "pythonhighlighter.h" #include "pythonindenter.h" @@ -118,6 +119,7 @@ public: return; PyLSConfigureAssistant::instance()->openDocumentWithPython(python, this); + PySideInstaller::instance()->checkPySideInstallation(python, this); } }; diff --git a/src/plugins/python/pythonrunconfiguration.cpp b/src/plugins/python/pythonrunconfiguration.cpp index 4c49a46eb2b..139d8a5dfb2 100644 --- a/src/plugins/python/pythonrunconfiguration.cpp +++ b/src/plugins/python/pythonrunconfiguration.cpp @@ -25,6 +25,7 @@ #include "pythonrunconfiguration.h" +#include "pyside.h" #include "pythonconstants.h" #include "pythonlanguageclient.h" #include "pythonproject.h" @@ -244,7 +245,7 @@ PythonRunConfiguration::PythonRunConfiguration(Target *target, Utils::Id id) auto interpreterAspect = addAspect(); interpreterAspect->setSettingsKey("PythonEditor.RunConfiguation.Interpreter"); connect(interpreterAspect, &InterpreterAspect::changed, - this, &PythonRunConfiguration::updateLanguageServer); + this, &PythonRunConfiguration::interpreterChanged); connect(PythonSettings::instance(), &PythonSettings::interpretersChanged, interpreterAspect, &InterpreterAspect::updateInterpreters); @@ -292,7 +293,7 @@ PythonRunConfiguration::PythonRunConfiguration(Target *target, Utils::Id id) connect(target, &Target::buildSystemUpdated, this, &RunConfiguration::update); } -void PythonRunConfiguration::updateLanguageServer() +void PythonRunConfiguration::interpreterChanged() { using namespace LanguageClient; @@ -300,8 +301,10 @@ void PythonRunConfiguration::updateLanguageServer() for (FilePath &file : project()->files(Project::AllFiles)) { if (auto document = TextEditor::TextDocument::textDocumentForFilePath(file)) { - if (document->mimeType() == Constants::C_PY_MIMETYPE) + if (document->mimeType() == Constants::C_PY_MIMETYPE) { PyLSConfigureAssistant::instance()->openDocumentWithPython(python, document); + PySideInstaller::instance()->checkPySideInstallation(python, document); + } } } } diff --git a/src/plugins/python/pythonrunconfiguration.h b/src/plugins/python/pythonrunconfiguration.h index ffdf4a4d27c..b498b3a0542 100644 --- a/src/plugins/python/pythonrunconfiguration.h +++ b/src/plugins/python/pythonrunconfiguration.h @@ -45,7 +45,7 @@ public: QString interpreter() const; private: - void updateLanguageServer(); + void interpreterChanged(); bool supportsDebugger() const; QString mainScript() const; diff --git a/src/plugins/python/pythonsettings.cpp b/src/plugins/python/pythonsettings.cpp index 5f4957d17a3..9832598aac4 100644 --- a/src/plugins/python/pythonsettings.cpp +++ b/src/plugins/python/pythonsettings.cpp @@ -549,6 +549,12 @@ Interpreter PythonSettings::defaultInterpreter() return interpreterOptionsPage().defaultInterpreter(); } +Interpreter PythonSettings::interpreter(const QString &interpreterId) +{ + const QList interpreters = PythonSettings::interpreters(); + return Utils::findOrDefault(interpreters, Utils::equal(&Interpreter::id, interpreterId)); +} + } // namespace Internal } // namespace Python diff --git a/src/plugins/python/pythonsettings.h b/src/plugins/python/pythonsettings.h index 0ef6c62a0ff..7f6ce314475 100644 --- a/src/plugins/python/pythonsettings.h +++ b/src/plugins/python/pythonsettings.h @@ -62,6 +62,7 @@ public: static QList interpreters(); static Interpreter defaultInterpreter(); + static Interpreter interpreter(const QString &interpreterId); static void setInterpreter(const QList &interpreters, const QString &defaultId); static void addInterpreter(const Interpreter &interpreter, bool isDefault = false); static PythonSettings *instance();