diff options
Diffstat (limited to 'sources/pyside6/plugins')
-rw-r--r-- | sources/pyside6/plugins/designer/CMakeLists.txt | 59 | ||||
-rw-r--r-- | sources/pyside6/plugins/designer/designercustomwidgets.cpp | 261 | ||||
-rw-r--r-- | sources/pyside6/plugins/designer/designercustomwidgets.h | 28 | ||||
-rw-r--r-- | sources/pyside6/plugins/uitools/CMakeLists.txt | 35 | ||||
-rw-r--r-- | sources/pyside6/plugins/uitools/customwidget.cpp | 104 | ||||
-rw-r--r-- | sources/pyside6/plugins/uitools/customwidget.h | 37 | ||||
-rw-r--r-- | sources/pyside6/plugins/uitools/customwidgets.cpp | 25 | ||||
-rw-r--r-- | sources/pyside6/plugins/uitools/customwidgets.h | 35 |
8 files changed, 584 insertions, 0 deletions
diff --git a/sources/pyside6/plugins/designer/CMakeLists.txt b/sources/pyside6/plugins/designer/CMakeLists.txt new file mode 100644 index 000000000..717652314 --- /dev/null +++ b/sources/pyside6/plugins/designer/CMakeLists.txt @@ -0,0 +1,59 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +project(PySidePlugin) + +# Note: At runtime, the dependency to the shiboken library is resolved +# by the pyside_tool.py wrapper + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOMOC ON) + +find_package(Qt6 COMPONENTS Core Gui Widgets UiPlugin) + +qt_add_plugin(PySidePlugin) + +target_sources(PySidePlugin PRIVATE + designercustomwidgets.cpp designercustomwidgets.h +) + +# See libshiboken/CMakeLists.txt + +target_compile_definitions(PySidePlugin PRIVATE -DQT_NO_KEYWORDS=1) + +if(PYTHON_LIMITED_API) + target_compile_definitions(PySidePlugin PRIVATE "-DPy_LIMITED_API=0x03050000") +endif() + +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + if(PYTHON_WITH_DEBUG) + target_compile_definitions(PySidePlugin PRIVATE "-DPy_DEBUG") + endif() + if (PYTHON_WITH_COUNT_ALLOCS) + target_compile_definitions(PySidePlugin PRIVATE "-DCOUNT_ALLOCS") + endif() +elseif(CMAKE_BUILD_TYPE STREQUAL "Release") + target_compile_definitions(PySidePlugin PRIVATE "-DNDEBUG") +endif() + +target_include_directories(PySidePlugin PRIVATE ../uitools) + +set_target_properties(PySidePlugin PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +get_property(SHIBOKEN_PYTHON_LIBRARIES GLOBAL PROPERTY shiboken_python_libraries) +get_property(SHIBOKEN_PYTHON_INCLUDE_DIRS GLOBAL PROPERTY shiboken_python_include_dirs) + +target_include_directories(PySidePlugin PRIVATE ${SHIBOKEN_PYTHON_INCLUDE_DIRS}) + +target_link_libraries(PySidePlugin PRIVATE + Qt::Core + Qt::Gui + Qt::UiPlugin + Qt::Widgets + ${SHIBOKEN_PYTHON_LIBRARIES}) + +install(TARGETS PySidePlugin LIBRARY DESTINATION "${QT6_INSTALL_PLUGINS}/designer") diff --git a/sources/pyside6/plugins/designer/designercustomwidgets.cpp b/sources/pyside6/plugins/designer/designercustomwidgets.cpp new file mode 100644 index 000000000..d23156a9d --- /dev/null +++ b/sources/pyside6/plugins/designer/designercustomwidgets.cpp @@ -0,0 +1,261 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#undef slots +#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> + +using namespace Qt::StringLiterals; + +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 != nullptr ? pyStringToQString(pstr) : QString(); +} + +static QString pyErrorMessage() +{ + QString result = "<error information not available>"_L1; + PyObject *ptype = {}; + PyObject *pvalue = {}; + PyObject *ptraceback = {}; + PyErr_Fetch(&ptype, &pvalue, &ptraceback); + if (pvalue != nullptr) + 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 = "Internal error: Cannot retrieve __main__"_L1; + 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("Error running "_L1 + fileName + ": "_L1); + } + 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. macOS seems to be showing the same issues. + + const auto os = QOperatingSystemVersion::currentType(); + + bool ok; + int majorVersion = qEnvironmentVariableIntValue("PY_MAJOR_VERSION", &ok); + int minorVersion = qEnvironmentVariableIntValue("PY_MINOR_VERSION", &ok); + if (!ok) { + majorVersion = PY_MAJOR_VERSION; + minorVersion = PY_MINOR_VERSION; + } + + if (!qEnvironmentVariableIsSet(virtualEnvVar) + || (os != QOperatingSystemVersion::MacOS && os != QOperatingSystemVersion::Windows) + || (majorVersion == 3 && minorVersion < 8)) { + return; + } + + const QByteArray virtualEnvPath = qgetenv(virtualEnvVar); + QByteArray pythonPath = qgetenv(pythonPathVar); + if (!pythonPath.isEmpty()) + pythonPath.append(QDir::listSeparator().toLatin1()); + + switch (os) { + case QOperatingSystemVersion::Windows: + pythonPath.append(virtualEnvPath + R"(\Lib\site-packages)"); + break; + case QOperatingSystemVersion::MacOS: + pythonPath.append(virtualEnvPath + "/lib/python"_ba + + QByteArray::number(majorVersion) + '.' + + QByteArray::number(minorVersion) + + "/site-packages"_ba); + break; + default: + break; + } + + qputenv(pythonPathVar, pythonPath); +} + +static void initPython() +{ + // Py_SetProgramName() is considered harmful, it can break virtualenv. + initVirtualEnvironment(); + + Py_Initialize(); + qAddPostRoutine(Py_Finalize); +} + +static bool withinQtDesigner = false; + +PyDesignerCustomWidgets::PyDesignerCustomWidgets(QObject *parent) : QObject(parent) +{ + qCDebug(lcPySidePlugin, "%s", __FUNCTION__); + + withinQtDesigner = QCoreApplication::applicationName() == u"Designer" + && QCoreApplication::organizationName() == u"QtProject"; + + if (!qEnvironmentVariableIsSet(pathVar)) { + if (withinQtDesigner) { + 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({u"register*.py"_s}, 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); + } + + // Might be initialized already, for example, when loaded from QUiLoader. + if (Py_IsInitialized() == 0) + initPython(); + + // Run all register*py files + QString errorMessage; + for (const auto &pythonFile : std::as_const(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(); + if (withinQtDesigner) + qCWarning(lcPySidePlugin, "No instance of QPyDesignerCustomWidgetCollection was found."); + return {}; +} diff --git a/sources/pyside6/plugins/designer/designercustomwidgets.h b/sources/pyside6/plugins/designer/designercustomwidgets.h new file mode 100644 index 000000000..2f1db1f31 --- /dev/null +++ b/sources/pyside6/plugins/designer/designercustomwidgets.h @@ -0,0 +1,28 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef PY_DESIGNER_CUSTOM_WIDGETS_H_ +#define PY_DESIGNER_CUSTOM_WIDGETS_H_ + +#include <QtUiPlugin/QDesignerCustomWidgetCollectionInterface> + +// A Qt Designer plugin proxying the QDesignerCustomWidgetCollectionInterface +// instance set as as a dynamic property on QCoreApplication by the PySide6 +// Qt Designer module. +class PyDesignerCustomWidgets: public QObject, + public QDesignerCustomWidgetCollectionInterface +{ + Q_OBJECT + Q_INTERFACES(QDesignerCustomWidgetCollectionInterface) + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.PySide.PyDesignerCustomWidgetsInterface") + +public: + Q_DISABLE_COPY_MOVE(PyDesignerCustomWidgets) + + explicit PyDesignerCustomWidgets(QObject *parent = nullptr); + ~PyDesignerCustomWidgets() override; + + QList<QDesignerCustomWidgetInterface *> customWidgets() const override; +}; + +#endif // PY_DESIGNER_CUSTOM_WIDGETS_H_ diff --git a/sources/pyside6/plugins/uitools/CMakeLists.txt b/sources/pyside6/plugins/uitools/CMakeLists.txt new file mode 100644 index 000000000..06d0ae900 --- /dev/null +++ b/sources/pyside6/plugins/uitools/CMakeLists.txt @@ -0,0 +1,35 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +project(plugins) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOMOC ON) + +find_package(Qt6 COMPONENTS Core Gui Widgets UiPlugin) + +set(ui_plugin_src + customwidgets.cpp customwidgets.h + customwidget.cpp customwidget.h +) + +add_library(uiplugin STATIC ${ui_plugin_src}) +if(CMAKE_HOST_UNIX AND NOT CYGWIN) + add_definitions(-fPIC) +endif() +add_definitions(-DQT_STATICPLUGIN) + +set_property(TARGET pyside6 PROPERTY CXX_STANDARD 17) + +target_link_libraries(uiplugin + Qt::Core + Qt::Gui + Qt::UiPlugin + Qt::Widgets + Shiboken6::libshiboken) +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + set(LIBRARY_OUTPUT_SUFFIX ${CMAKE_DEBUG_POSTFIX}) +else() + set(LIBRARY_OUTPUT_SUFFIX ${CMAKE_RELEASE_POSTFIX}) +endif() diff --git a/sources/pyside6/plugins/uitools/customwidget.cpp b/sources/pyside6/plugins/uitools/customwidget.cpp new file mode 100644 index 000000000..976754feb --- /dev/null +++ b/sources/pyside6/plugins/uitools/customwidget.cpp @@ -0,0 +1,104 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "customwidget.h" +#include <QtCore/qdebug.h> + +// Part of the static plugin linked to the QtUiLoader Python module, +// allowing it to create a custom widget written in Python. +PyCustomWidget::PyCustomWidget(PyObject *objectType) : + m_pyObject(objectType), + m_name(QString::fromUtf8(reinterpret_cast<PyTypeObject *>(objectType)->tp_name)) +{ +} + +bool PyCustomWidget::isContainer() const +{ + return false; +} + +bool PyCustomWidget::isInitialized() const +{ + return m_initialized; +} + +QIcon PyCustomWidget::icon() const +{ + return {}; +} + +QString PyCustomWidget::domXml() const +{ + return {}; +} + +QString PyCustomWidget::group() const +{ + return {}; +} + +QString PyCustomWidget::includeFile() const +{ + return {}; +} + +QString PyCustomWidget::name() const +{ + return m_name; +} + +QString PyCustomWidget::toolTip() const +{ + return {}; +} + +QString PyCustomWidget::whatsThis() const +{ + return {}; +} + +// A copy of this code exists in PyDesignerCustomWidget::createWidget() +// (see sources/pyside6/PySide6/QtDesigner/qpydesignercustomwidgetcollection.cpp). +QWidget *PyCustomWidget::createWidget(QWidget *parent) +{ + // Create a python instance and return cpp object + PyObject *pyParent = nullptr; + bool unknownParent = false; + if (parent != nullptr) { + pyParent = reinterpret_cast<PyObject *>(Shiboken::BindingManager::instance().retrieveWrapper(parent)); + if (pyParent != nullptr) { + Py_INCREF(pyParent); + } else { + static Shiboken::Conversions::SpecificConverter converter("QWidget*"); + pyParent = converter.toPython(&parent); + unknownParent = true; + } + } else { + Py_INCREF(Py_None); + pyParent = Py_None; + } + + Shiboken::AutoDecRef pyArgs(PyTuple_New(1)); + PyTuple_SET_ITEM(pyArgs.object(), 0, pyParent); // tuple will keep pyParent reference + + // Call python constructor + auto *result = reinterpret_cast<SbkObject *>(PyObject_CallObject(m_pyObject, pyArgs)); + if (result == nullptr) { + qWarning("Unable to create a Python custom widget of type \"%s\".", + qPrintable(m_name)); + PyErr_Print(); + return nullptr; + } + + if (unknownParent) // if parent does not exist in python, transfer the ownership to cpp + Shiboken::Object::releaseOwnership(result); + else + Shiboken::Object::setParent(pyParent, reinterpret_cast<PyObject *>(result)); + + return reinterpret_cast<QWidget *>(Shiboken::Object::cppPointer(result, Py_TYPE(result))); +} + +void PyCustomWidget::initialize(QDesignerFormEditorInterface *) +{ + m_initialized = true; +} diff --git a/sources/pyside6/plugins/uitools/customwidget.h b/sources/pyside6/plugins/uitools/customwidget.h new file mode 100644 index 000000000..52621f0bd --- /dev/null +++ b/sources/pyside6/plugins/uitools/customwidget.h @@ -0,0 +1,37 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef PY_CUSTOM_WIDGET_H_ +#define PY_CUSTOM_WIDGET_H_ + +#include <shiboken.h> + +#include <QtUiPlugin/QDesignerCustomWidgetInterface> + +class PyCustomWidget: public QObject, public QDesignerCustomWidgetInterface +{ + Q_OBJECT + Q_INTERFACES(QDesignerCustomWidgetInterface) + +public: + explicit PyCustomWidget(PyObject *objectType); + + bool isContainer() const override; + bool isInitialized() const override; + QIcon icon() const override; + QString domXml() const override; + QString group() const override; + QString includeFile() const override; + QString name() const override; + QString toolTip() const override; + QString whatsThis() const override; + QWidget *createWidget(QWidget *parent) override; + void initialize(QDesignerFormEditorInterface *core) override; + +private: + PyObject *m_pyObject = nullptr; + const QString m_name; + bool m_initialized = false; +}; + +#endif // PY_CUSTOM_WIDGET_H_ diff --git a/sources/pyside6/plugins/uitools/customwidgets.cpp b/sources/pyside6/plugins/uitools/customwidgets.cpp new file mode 100644 index 000000000..93b6b4a10 --- /dev/null +++ b/sources/pyside6/plugins/uitools/customwidgets.cpp @@ -0,0 +1,25 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "customwidgets.h" +#include "customwidget.h" + +PyCustomWidgets::PyCustomWidgets(QObject *parent) + : QObject(parent) +{ +} + +PyCustomWidgets::~PyCustomWidgets() +{ + qDeleteAll(m_widgets); +} + +void PyCustomWidgets::registerWidgetType(PyObject *widget) +{ + m_widgets.append(new PyCustomWidget(widget)); +} + +QList<QDesignerCustomWidgetInterface *> PyCustomWidgets::customWidgets() const +{ + return m_widgets; +} diff --git a/sources/pyside6/plugins/uitools/customwidgets.h b/sources/pyside6/plugins/uitools/customwidgets.h new file mode 100644 index 000000000..f67a0847d --- /dev/null +++ b/sources/pyside6/plugins/uitools/customwidgets.h @@ -0,0 +1,35 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef PY_CUSTOM_WIDGETS_H_ +#define PY_CUSTOM_WIDGETS_H_ + +#include <shiboken.h> + +#include <QtUiPlugin/QDesignerCustomWidgetInterface> + +#include <QtCore/qlist.h> + +// A static plugin linked to the QtUiLoader Python module +class PyCustomWidgets: public QObject, public QDesignerCustomWidgetCollectionInterface +{ + Q_OBJECT + Q_INTERFACES(QDesignerCustomWidgetCollectionInterface) + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.PySide.PyCustomWidgetsInterface") + +public: + Q_DISABLE_COPY_MOVE(PyCustomWidgets) + + explicit PyCustomWidgets(QObject *parent = nullptr); + ~PyCustomWidgets() override; + + QList<QDesignerCustomWidgetInterface*> customWidgets() const override; + + // Called from added function QUiLoader::registerCustomWidget() + void registerWidgetType(PyObject* widget); + +private: + QList<QDesignerCustomWidgetInterface *> m_widgets; +}; + +#endif |