diff options
| author | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2021-02-05 17:06:16 +0100 |
|---|---|---|
| committer | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2021-02-16 13:35:45 +0000 |
| commit | 82afd88245a17b6ba759937a2b391c216857565a (patch) | |
| tree | 1700964ad26a47e30e470fe679a80c72da17a26a /sources/pyside6/plugins/designer/designercustomwidgets.cpp | |
| parent | b9aa61247c355a9330e52059951649c6ba7697c2 (diff) | |
PySide6: Add a Designer plugin
Add a convencience class QPyDesignerCustomWidgetCollection to
the Qt Designer module, which provides functions for
registering widget types or adding QDesignerCustomWidgetInterface
instances. A static instance of it is stored as a dynamic
property on QCoreApplication, which is retrieved by a Qt Designer
plugin, which provides the collection of widgets registered in
Python.
Task-number: PYSIDE-1455
Change-Id: If4055e6c9db6a03b32016b013a1130051bbd472a
Reviewed-by: Cristian Maureira-Fredes <cristian.maureira-fredes@qt.io>
Diffstat (limited to 'sources/pyside6/plugins/designer/designercustomwidgets.cpp')
| -rw-r--r-- | sources/pyside6/plugins/designer/designercustomwidgets.cpp | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/sources/pyside6/plugins/designer/designercustomwidgets.cpp b/sources/pyside6/plugins/designer/designercustomwidgets.cpp new file mode 100644 index 000000000..92455cc24 --- /dev/null +++ b/sources/pyside6/plugins/designer/designercustomwidgets.cpp @@ -0,0 +1,260 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt for Python. +** +** $QT_BEGIN_LICENSE:LGPL$ +** 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 Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** 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-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <Python.h> // Include before Qt headers due to 'slots' macro definition + +#include "designercustomwidgets.h" + +#include <QtCore/QCoreApplication> +#include <QtCore/QDebug> +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QFileInfoList> +#include <QtCore/QLoggingCategory> +#include <QtCore/QOperatingSystemVersion> +#include <QtCore/QTextStream> +#include <QtCore/QVariant> + +#include <string_view> + +Q_LOGGING_CATEGORY(lcPySidePlugin, "qt.pysideplugin") + +static const char pathVar[] = "PYSIDE_DESIGNER_PLUGINS"; +static const char pythonPathVar[] = "PYTHONPATH"; + +// Find the static instance of 'QPyDesignerCustomWidgetCollection' +// registered as a dynamic property of QCoreApplication. +static QDesignerCustomWidgetCollectionInterface *findPyDesignerCustomWidgetCollection() +{ + static const char propertyName[] = "__qt_PySideCustomWidgetCollection"; + if (auto coreApp = QCoreApplication::instance()) { + const QVariant value = coreApp->property(propertyName); + if (value.isValid() && value.canConvert<void *>()) + return reinterpret_cast<QDesignerCustomWidgetCollectionInterface *>(value.value<void *>()); + } + return nullptr; +} + +static QString pyStringToQString(PyObject *s) +{ + // PyUnicode_AsUTF8() is not available in the Limited API + if (PyObject *bytesStr = PyUnicode_AsEncodedString(s, "utf8", nullptr)) + return QString::fromUtf8(PyBytes_AsString(bytesStr)); + return {}; +} + +// Return str() of a Python object +static QString pyStr(PyObject *o) +{ + PyObject *pstr = PyObject_Str(o); + return pstr ? pyStringToQString(pstr) : QString(); +} + +static QString pyErrorMessage() +{ + QString result = QLatin1String("<error information not available>"); + PyObject *ptype = {}; + PyObject *pvalue = {}; + PyObject *ptraceback = {}; + PyErr_Fetch(&ptype, &pvalue, &ptraceback); + if (pvalue) + result = pyStr(pvalue); + PyErr_Restore(ptype, pvalue, ptraceback); + return result; +} + + +#ifdef Py_LIMITED_API +// Provide PyRun_String() for limited API (see libshiboken/pep384impl.cpp) +// Flags are ignored in these simple helpers. +PyObject *PyRun_String(const char *str, int start, PyObject *globals, PyObject *locals) +{ + PyObject *code = Py_CompileString(str, "pyscript", start); + PyObject *ret = nullptr; + + if (code != nullptr) { + ret = PyEval_EvalCode(code, globals, locals); + } + Py_XDECREF(code); + return ret; +} +#endif // Py_LIMITED_API + +static bool runPyScript(const char *script, QString *errorMessage) +{ + PyObject *main = PyImport_AddModule("__main__"); + if (main == nullptr) { + *errorMessage = QLatin1String("Internal error: Cannot retrieve __main__"); + return false; + } + PyObject *globalDictionary = PyModule_GetDict(main); + PyObject *localDictionary = PyDict_New(); + // Note: Limited API only has PyRun_String() + PyObject *result = PyRun_String(script, Py_file_input, globalDictionary, localDictionary); + const bool ok = result != nullptr; + Py_DECREF(localDictionary); + Py_XDECREF(result); + if (!ok) { + *errorMessage = pyErrorMessage(); + PyErr_Clear(); + } + return ok; +} + +static bool runPyScriptFile(const QString &fileName, QString *errorMessage) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly| QIODevice::Text)) { + QTextStream(errorMessage) << "Cannot open " + << QDir::toNativeSeparators(fileName) << " for reading: " + << file.errorString(); + return false; + } + + const QByteArray script = file.readAll(); + file.close(); + const bool ok = runPyScript(script.constData(), errorMessage); + if (!ok && !errorMessage->isEmpty()) { + errorMessage->prepend(QLatin1String("Error running ") + fileName + + QLatin1String(": ")); + } + return ok; +} + +static void initVirtualEnvironment() +{ + static const char virtualEnvVar[] = "VIRTUAL_ENV"; + // As of Python 3.8/Windows, Python is no longer able to run stand-alone in + // a virtualenv due to missing libraries. Add the path to the modules + // instead. + if (!qEnvironmentVariableIsSet(virtualEnvVar) + || QOperatingSystemVersion::currentType() != QOperatingSystemVersion::Windows + || (PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION < 8)) { + return; + } + + const QByteArray virtualEnvPath = qgetenv(virtualEnvVar); + QByteArray pythonPath = qgetenv(pythonPathVar); + if (!pythonPath.isEmpty()) + pythonPath.append(QDir::listSeparator().toLatin1()); + pythonPath.append(virtualEnvPath + R"(\Lib\site-packages)"); + qputenv(pythonPathVar, pythonPath); +} + +static void initPython() +{ + // Py_SetProgramName() is considered harmful, it can break virtualenv. + initVirtualEnvironment(); + + Py_Initialize(); + qAddPostRoutine(Py_Finalize); +} + +PyDesignerCustomWidgets::PyDesignerCustomWidgets(QObject *parent) : QObject(parent) +{ + qCDebug(lcPySidePlugin, "%s", __FUNCTION__); + + if (!qEnvironmentVariableIsSet(pathVar)) { + qCWarning(lcPySidePlugin, "Environment variable %s is not set, bailing out.", + pathVar); + return; + } + + QStringList pythonFiles; + const QString pathStr = qEnvironmentVariable(pathVar); + const QChar listSeparator = QDir::listSeparator(); + const auto paths = pathStr.split(listSeparator); + const QStringList oldPythonPaths = + qEnvironmentVariable(pythonPathVar).split(listSeparator, Qt::SkipEmptyParts); + QStringList pythonPaths = oldPythonPaths; + // Scan for register*.py in the path + for (const auto &p : paths) { + QDir dir(p); + if (dir.exists()) { + const QFileInfoList matches = + dir.entryInfoList({QStringLiteral("register*.py")}, QDir::Files, + QDir::Name); + for (const auto &fi : matches) + pythonFiles.append(fi.absoluteFilePath()); + if (!matches.isEmpty()) { + const QString dir = + QDir::toNativeSeparators(matches.constFirst().absolutePath()); + if (!oldPythonPaths.contains(dir)) + pythonPaths.append(dir); + } + } else { + qCWarning(lcPySidePlugin, "Directory '%s' as specified in %s does not exist.", + qPrintable(p), pathVar); + } + } + if (pythonFiles.isEmpty()) { + qCWarning(lcPySidePlugin, "No python files found in '%s'.", qPrintable(pathStr)); + return; + } + + // Make modules available by adding them to the path + if (pythonPaths != oldPythonPaths) { + const QByteArray value = pythonPaths.join(listSeparator).toLocal8Bit(); + qCDebug(lcPySidePlugin) << "setting" << pythonPathVar << value; + qputenv(pythonPathVar, value); + } + + initPython(); + + // Run all register*py files + QString errorMessage; + for (const auto &pythonFile : qAsConst(pythonFiles)) { + qCDebug(lcPySidePlugin) << "running" << pythonFile; + if (!runPyScriptFile(pythonFile, &errorMessage)) + qCWarning(lcPySidePlugin, "%s", qPrintable(errorMessage)); + } +} + +PyDesignerCustomWidgets::~PyDesignerCustomWidgets() +{ + qCDebug(lcPySidePlugin, "%s", __FUNCTION__); +} + +QList<QDesignerCustomWidgetInterface *> PyDesignerCustomWidgets::customWidgets() const +{ + if (auto collection = findPyDesignerCustomWidgetCollection()) + return collection->customWidgets(); + qCWarning(lcPySidePlugin, "No instance of QPyDesignerCustomWidgetCollection was found."); + return {}; +} |
