WigglyWidget Example

This example shows how to interact with a custom widget from two different ways:

  • A full Python translation from a C++ example,

  • A Python binding generated from the C++ file.

The original example contained three different files:

  • main.cpp/h, which was translated to main.py,

  • dialog.cpp/h, which was translated to dialog.py,

  • wigglywidget.cpp/h, which was translated to wigglywidget.py, but also remains as is, to enable the binding generation through Shiboken.

In the dialog.py file you will find two imports that will be related to each of the two approaches described before::

# Python translated file
from wigglywidget import WigglyWidget

# Binding module create with Shiboken
from wiggly import WigglyWidget

Steps to build the bindings

The most important files are:

  • bindings.xml, to specify the class that we want to expose from C++ to Python,

  • bindings.h to include the header of the classes we want to expose

  • CMakeList.txt, with all the instructions to build the shared libraries (DLL, or dylib)

  • pyside_config.py which is located in the utils directory, one level up, to get the path for Shiboken and PySide.

Now create a build/ directory, and from inside run cmake to use the provided CMakeLists.txt:

Run CMake on macOS/Linux:

cd ~/pyside-setup/examples/widgetbinding
cd build
cmake .. -B. -G Ninja -DCMAKE_BUILD_TYPE=Release

Run CMake on Windows:

cd C:\pyside-setup\examples\widgetbinding
mkdir build
cd build
cmake .. -B. -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=cl.exe

To build:

ninja
ninja install
cd ..

The final example can then be run by:

python main.py

You should see two identical custom widgets, one being the Python translation, and the other one being the C++ one.

Final words

Since this example originated by mixing the concepts of the scriptableapplication and samplebinding examples, you can complement this README with the ones in those directories.

Download this example

// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#ifndef BINDINGS_H
#define BINDINGS_H
#include "wigglywidget.h"
#endif // BINDINGS_H
<?xml version="1.0" encoding="UTF-8"?>
<!--
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
-->
<typesystem package="wiggly">
    <load-typesystem name="typesystem_widgets.xml" generate="no"/>
    <object-type name="WigglyWidget"/>
</typesystem>
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtWidgets import QDialog, QLineEdit, QVBoxLayout

# Python binding from the C++ widget
from wiggly import WigglyWidget as WigglyWidgetCPP

# Python-only widget
from wigglywidget import WigglyWidget as WigglyWidgetPY


class Dialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        wiggly_widget_py = WigglyWidgetPY(self)
        wiggly_widget_cpp = WigglyWidgetCPP(self)
        lineEdit = QLineEdit(self)

        layout = QVBoxLayout(self)
        layout.addWidget(wiggly_widget_py)
        layout.addWidget(wiggly_widget_cpp)
        layout.addWidget(lineEdit)
        lineEdit.setClearButtonEnabled(True)
        wiggly_widget_py.running = True
        wiggly_widget_cpp.setRunning(True)

        lineEdit.textChanged.connect(wiggly_widget_py.setText)
        lineEdit.textChanged.connect(wiggly_widget_cpp.setText)
        lineEdit.setText("🖖 Hello world!")

        self.setWindowTitle("Wiggly")
        self.resize(360, 145)
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#ifndef MACROS_H
#define MACROS_H

#include <QtCore/qglobal.h>

// Export symbols when creating .dll and .lib, and import them when using .lib.
#if BINDINGS_BUILD
#    define BINDINGS_API Q_DECL_EXPORT
#else
#    define BINDINGS_API Q_DECL_IMPORT
#endif

#endif // MACROS_H
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

import sys

from PySide6.QtWidgets import QApplication

from dialog import Dialog

if __name__ == "__main__":
    app = QApplication()
    w = Dialog()
    w.show()
    sys.exit(app.exec())
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from wigglywidget import WigglyWidget

# Set PYSIDE_DESIGNER_PLUGINS to point to this directory and load the plugin


TOOLTIP = "A cool wiggly widget (Python)"
DOM_XML = """
<ui language='c++'>
    <widget class='WigglyWidget' name='wigglyWidget'>
        <property name='geometry'>
            <rect>
                <x>0</x>
                <y>0</y>
                <width>400</width>
                <height>200</height>
            </rect>
        </property>
        <property name='text'>
            <string>Hello, world</string>
        </property>
    </widget>
</ui>
"""

if __name__ == '__main__':
    QPyDesignerCustomWidgetCollection.registerCustomWidget(WigglyWidget, module="wigglywidget",
                                                           tool_tip=TOOLTIP, xml=DOM_XML)
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#include "wigglywidget.h"

#include <QtGui/QFontMetrics>
#include <QtGui/QPainter>
#include <QtCore/QTimerEvent>

//! [0]
WigglyWidget::WigglyWidget(QWidget *parent)
    : QWidget(parent)
{
    setBackgroundRole(QPalette::Midlight);
    setAutoFillBackground(true);

    QFont newFont = font();
    newFont.setPointSize(newFont.pointSize() + 20);
    setFont(newFont);
}
//! [0]

//! [1]
void WigglyWidget::paintEvent(QPaintEvent * /* event */)
//! [1] //! [2]
{
    if (m_text.isEmpty())
        return;
    static constexpr int sineTable[16] = {
        0, 38, 71, 92, 100, 92, 71, 38, 0, -38, -71, -92, -100, -92, -71, -38
    };

    QFontMetrics metrics(font());
    int x = (width() - metrics.horizontalAdvance(m_text)) / 2;
    int y = (height() + metrics.ascent() - metrics.descent()) / 2;
    QColor color;
//! [2]

//! [3]
    QPainter painter(this);
//! [3] //! [4]
    int offset = 0;
    const auto codePoints = m_text.toUcs4();
    for (char32_t codePoint : codePoints) {
        const int index = (m_step + offset++) % 16;
        color.setHsv((15 - index) * 16, 255, 191);
        painter.setPen(color);
        QString symbol = QString::fromUcs4(&codePoint, 1);
        const int dy = (sineTable[index] * metrics.height()) / 400;
        painter.drawText(x, y - dy, symbol);
        x += metrics.horizontalAdvance(symbol);
    }
}
//! [4]

//! [5]
void WigglyWidget::timerEvent(QTimerEvent *event)
//! [5] //! [6]
{
    if (event->timerId() == m_timer.timerId()) {
        ++m_step;
        update();
    } else {
        QWidget::timerEvent(event);
    }
//! [6]
}

QString WigglyWidget::text() const
{
    return m_text;
}

void WigglyWidget::setText(const QString &newText)
{
    m_text = newText;
}

bool WigglyWidget::isRunning() const
{
    return m_timer.isActive();
}

void WigglyWidget::setRunning(bool r)
{
    if (r == isRunning())
        return;
    if (r)
        m_timer.start(60, this);
    else
        m_timer.stop();
}
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#ifndef WIGGLYWIDGET_H
#define WIGGLYWIDGET_H

#include "macros.h"

#include <QtWidgets/QWidget>
#include <QtCore/QBasicTimer>

//! [0]
class BINDINGS_API WigglyWidget : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(bool running READ isRunning WRITE setRunning)
    Q_PROPERTY(QString text READ text WRITE setText)

public:
    WigglyWidget(QWidget *parent = nullptr);

    QString text() const;
    bool isRunning() const;

public slots:
    void setText(const QString &newText);
    void setRunning(bool r);

protected:
    void paintEvent(QPaintEvent *event) override;
    void timerEvent(QTimerEvent *event) override;

private:
    QBasicTimer m_timer;
    QString m_text;
    int m_step = 0;
};
//! [0]

#endif
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtCore import QBasicTimer, Property
from PySide6.QtGui import QColor, QFontMetrics, QPainter, QPalette
from PySide6.QtWidgets import QWidget


class WigglyWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._step = 0
        self._text = ""
        self.setBackgroundRole(QPalette.Midlight)
        self.setAutoFillBackground(True)

        new_font = self.font()
        new_font.setPointSize(new_font.pointSize() + 20)
        self.setFont(new_font)
        self._timer = QBasicTimer()

    def isRunning(self):
        return self._timer.isActive()

    def setRunning(self, r):
        if r == self.isRunning():
            return
        if r:
            self._timer.start(60, self)
        else:
            self._timer.stop()

    def paintEvent(self, event):
        if not self._text:
            return

        sineTable = [0, 38, 71, 92, 100, 92, 71, 38, 0, -38, -71, -92, -100,
                     -92, -71, -38]

        metrics = QFontMetrics(self.font())
        x = (self.width() - metrics.horizontalAdvance(self.text)) / 2
        y = (self.height() + metrics.ascent() - metrics.descent()) / 2
        color = QColor()

        with QPainter(self) as painter:
            for i in range(len(self.text)):
                index = (self._step + i) % 16
                color.setHsv((15 - index) * 16, 255, 191)
                painter.setPen(color)
                dy = (sineTable[index] * metrics.height()) / 400
                c = self._text[i]
                painter.drawText(x, y - dy, str(c))
                x += metrics.horizontalAdvance(c)

    def timerEvent(self, event):
        if event.timerId() == self._timer.timerId():
            self._step += 1
            self.update()
        else:
            QWidget.timerEvent(event)

    def text(self):
        return self._text

    def setText(self, text):
        self._text = text

    running = Property(bool, isRunning, setRunning)
    text = Property(str, text, setText)
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause

cmake_minimum_required(VERSION 3.18)
cmake_policy(VERSION 3.18)

# Enable policy to not use RPATH settings for install_name on macOS.
if(POLICY CMP0068)
  cmake_policy(SET CMP0068 NEW)
endif()

# Enable policy to run automoc on generated files.
if(POLICY CMP0071)
  cmake_policy(SET CMP0071 NEW)
endif()

# Consider changing the project name to something relevant for you.
project(wiggly LANGUAGES CXX)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)

find_package(Qt6 COMPONENTS Core Gui Widgets)

# ================================ General configuration ======================================

# Set CPP standard to C++17 minimum.
set(CMAKE_CXX_STANDARD 17)

# The wiggly library for which we will create bindings. You can change the name to something
# relevant for your project.
set(wiggly_library "libwiggly")

# The name of the generated bindings module (as imported in Python). You can change the name
# to something relevant for your project.
set(bindings_library "wiggly")

# The header file with all the types and functions for which bindings will be generated.
# Usually it simply includes other headers of the library you are creating bindings for.
set(wrapped_header ${CMAKE_SOURCE_DIR}/bindings.h)

# The typesystem xml file which defines the relationships between the C++ types / functions
# and the corresponding Python equivalents.
set(typesystem_file ${CMAKE_SOURCE_DIR}/bindings.xml)

# Specify which C++ files will be generated by shiboken. This includes the module wrapper
# and a '.cpp' file per C++ type. These are needed for generating the module shared
# library.
set(generated_sources
    ${CMAKE_CURRENT_BINARY_DIR}/${bindings_library}/wiggly_module_wrapper.cpp
    ${CMAKE_CURRENT_BINARY_DIR}/${bindings_library}/wigglywidget_wrapper.cpp)


# ================================== Shiboken detection ======================================
# Use provided python interpreter if given.
if(NOT python_interpreter)
    if(WIN32 AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
        find_program(python_interpreter "python_d")
        if(NOT python_interpreter)
            message(FATAL_ERROR
                "A debug Python interpreter could not be found, which is a requirement when "
                "building this example in a debug configuration. Make sure python_d.exe is in "
                "PATH.")
        endif()
    else()
        find_program(python_interpreter "python")
        if(NOT python_interpreter)
            message(FATAL_ERROR
                "No Python interpreter could be found. Make sure python is in PATH.")
        endif()
    endif()
endif()
message(STATUS "Using python interpreter: ${python_interpreter}")

# Macro to get various pyside / python include / link flags and paths.
# Uses the not entirely supported utils/pyside_config.py file.
macro(pyside_config option output_var)
    if(${ARGC} GREATER 2)
        set(is_list ${ARGV2})
    else()
        set(is_list "")
    endif()

    execute_process(
      COMMAND ${python_interpreter} "${CMAKE_SOURCE_DIR}/../utils/pyside_config.py"
              ${option}
      OUTPUT_VARIABLE ${output_var}
      OUTPUT_STRIP_TRAILING_WHITESPACE)

    if ("${${output_var}}" STREQUAL "")
        message(FATAL_ERROR "Error: Calling pyside_config.py ${option} returned no output.")
    endif()
    if(is_list)
        string (REPLACE " " ";" ${output_var} "${${output_var}}")
    endif()
endmacro()

# Query for the shiboken generator path, Python path, include paths and linker flags.
pyside_config(--shiboken-module-path shiboken_module_path)
pyside_config(--shiboken-generator-path shiboken_generator_path)
pyside_config(--pyside-path pyside_path)
pyside_config(--pyside-include-path pyside_include_dir 1)
pyside_config(--python-include-path python_include_dir)
pyside_config(--shiboken-generator-include-path shiboken_include_dir 1)
pyside_config(--shiboken-module-shared-libraries-cmake shiboken_shared_libraries 0)
pyside_config(--python-link-flags-cmake python_linking_data 0)
pyside_config(--pyside-shared-libraries-cmake pyside_shared_libraries 0)

set(shiboken_path "${shiboken_generator_path}/shiboken6${CMAKE_EXECUTABLE_SUFFIX}")
if(NOT EXISTS ${shiboken_path})
    message(FATAL_ERROR "Shiboken executable not found at path: ${shiboken_path}")
endif()


# ==================================== RPATH configuration ====================================


# =============================================================================================
# !!! (The section below is deployment related, so in a real world application you will want to
# take care of this properly with some custom script or tool).
# =============================================================================================
# Enable rpaths so that the built shared libraries find their dependencies.
set(CMAKE_SKIP_BUILD_RPATH FALSE)
set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
set(CMAKE_INSTALL_RPATH ${shiboken_module_path} ${CMAKE_CURRENT_SOURCE_DIR})
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
# =============================================================================================
# !!! End of dubious section.
# =============================================================================================


# =============================== CMake target - wiggly_library ===============================


# Get the relevant Qt include dirs, to pass them on to shiboken.
get_property(QT_WIDGETS_INCLUDE_DIRS TARGET Qt6::Widgets PROPERTY INTERFACE_INCLUDE_DIRECTORIES)
set(INCLUDES "")
foreach(INCLUDE_DIR ${QT_WIDGETS_INCLUDE_DIRS})
    list(APPEND INCLUDES "-I${INCLUDE_DIR}")
endforeach()

# On macOS, check if Qt is a framework build. This affects how include paths should be handled.
get_target_property(QtCore_is_framework Qt6::Core FRAMEWORK)
if (QtCore_is_framework)
    get_target_property(qt_core_library_location Qt6::Core LOCATION)
    get_filename_component(qt_core_library_location_dir "${qt_core_library_location}" DIRECTORY)
    get_filename_component(lib_dir "${qt_core_library_location_dir}/../" ABSOLUTE)
    list(APPEND INCLUDES "--framework-include-paths=${lib_dir}")
endif()

# We need to include the headers for the module bindings that we use.
set(pyside_additional_includes "")
foreach(INCLUDE_DIR ${pyside_include_dir})
    list(APPEND pyside_additional_includes "${INCLUDE_DIR}/QtCore")
    list(APPEND pyside_additional_includes "${INCLUDE_DIR}/QtGui")
    list(APPEND pyside_additional_includes "${INCLUDE_DIR}/QtWidgets")
endforeach()


# Define the wiggly shared library for which we will create bindings.
set(${wiggly_library}_sources  wigglywidget.cpp)
add_library(${wiggly_library} SHARED ${${wiggly_library}_sources})
set_property(TARGET ${wiggly_library} PROPERTY PREFIX "")

# Needed mostly on Windows to export symbols, and create a .lib file, otherwise the binding
# library can't link to the wiggly library.
target_compile_definitions(${wiggly_library} PRIVATE BINDINGS_BUILD)


# ====================== Shiboken target for generating binding C++ files  ====================


# Set up the options to pass to shiboken.
set(shiboken_options --generator-set=shiboken --enable-parent-ctor-heuristic
    --enable-pyside-extensions --enable-return-value-heuristic --use-isnull-as-nb_nonzero
    --avoid-protected-hack
    ${INCLUDES}
    -I${CMAKE_SOURCE_DIR}
    -T${CMAKE_SOURCE_DIR}
    -T${pyside_path}/typesystems
    --output-directory=${CMAKE_CURRENT_BINARY_DIR}
    )

set(generated_sources_dependencies ${wrapped_header} ${typesystem_file})

# Add custom target to run shiboken to generate the binding cpp files.
add_custom_command(OUTPUT ${generated_sources}
                    COMMAND ${shiboken_path}
                    ${shiboken_options} ${wrapped_header} ${typesystem_file}
                    DEPENDS ${generated_sources_dependencies}
                    #IMPLICIT_DEPENDS CXX ${wrapped_header}
                    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
                    COMMENT "Running generator for ${typesystem_file}.")


# =============================== CMake target - bindings_library =============================


# Set the cpp files which will be used for the bindings library.
set(${bindings_library}_sources ${generated_sources})

# Define and build the bindings library.
add_library(${bindings_library} SHARED ${${bindings_library}_sources})


# Apply relevant include and link flags.
target_include_directories(${bindings_library} PRIVATE ${pyside_additional_includes})
target_include_directories(${bindings_library} PRIVATE ${pyside_include_dir})
target_include_directories(${bindings_library} PRIVATE ${python_include_dir})
target_include_directories(${bindings_library} PRIVATE ${shiboken_include_dir})

target_link_libraries(${wiggly_library} PRIVATE Qt6::Widgets)
target_link_libraries(${bindings_library} PRIVATE Qt6::Widgets)
target_link_libraries(${bindings_library} PRIVATE ${wiggly_library})
target_link_libraries(${bindings_library} PRIVATE ${pyside_shared_libraries})
target_link_libraries(${bindings_library} PRIVATE ${shiboken_shared_libraries})

# Adjust the name of generated module.
set_property(TARGET ${bindings_library} PROPERTY PREFIX "")
set_property(TARGET ${bindings_library} PROPERTY OUTPUT_NAME
             "${bindings_library}${PYTHON_EXTENSION_SUFFIX}")
if(WIN32)
    if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
        set_property(TARGET ${bindings_library} PROPERTY SUFFIX "_d.pyd")
    else()
        set_property(TARGET ${bindings_library} PROPERTY SUFFIX ".pyd")
    endif()
endif()

# Make sure the linker doesn't complain about not finding Python symbols on macOS.
if(APPLE)
  set_target_properties(${bindings_library} PROPERTIES LINK_FLAGS "-undefined dynamic_lookup")
endif(APPLE)

# Find and link to the python import library only on Windows.
# On Linux and macOS, the undefined symbols will get resolved by the dynamic linker
# (the symbols will be picked up in the Python executable).
if (WIN32)
    list(GET python_linking_data 0 python_libdir)
    list(GET python_linking_data 1 python_lib)
    find_library(python_link_flags ${python_lib} PATHS ${python_libdir} HINTS ${python_libdir})
    target_link_libraries(${bindings_library} PRIVATE ${python_link_flags})
endif()

# ================================= Dubious deployment section ================================

set(windows_shiboken_shared_libraries)

if(WIN32)
    # =========================================================================================
    # !!! (The section below is deployment related, so in a real world application you will
    # want to take care of this properly (this is simply to eliminate errors that users usually
    # encounter.
    # =========================================================================================
    # Circumvent some "#pragma comment(lib)"s in "include/pyconfig.h" which might force to link
    # against a wrong python shared library.

    set(python_versions_list 3 36 37 38 39)
    set(python_additional_link_flags "")
    foreach(ver ${python_versions_list})
        set(python_additional_link_flags
            "${python_additional_link_flags} /NODEFAULTLIB:\"python${ver}_d.lib\"")
        set(python_additional_link_flags
            "${python_additional_link_flags} /NODEFAULTLIB:\"python${ver}.lib\"")
    endforeach()

    set_target_properties(${bindings_library}
                           PROPERTIES LINK_FLAGS "${python_additional_link_flags}")

    # Compile a list of shiboken shared libraries to be installed, so that
    # the user doesn't have to set the PATH manually to point to the PySide package.
    foreach(library_path ${shiboken_shared_libraries})
        string(REGEX REPLACE ".lib$" ".dll" library_path ${library_path})
        file(TO_CMAKE_PATH ${library_path} library_path)
        list(APPEND windows_shiboken_shared_libraries "${library_path}")
    endforeach()
    # =========================================================================================
    # !!! End of dubious section.
    # =========================================================================================
endif()

# =============================================================================================
# !!! (The section below is deployment related, so in a real world application you will want to
# take care of this properly with some custom script or tool).
# =============================================================================================
# Install the library and the bindings module into the source folder near the main.py file, so
# that the Python interpeter successfully imports the used module.
install(TARGETS ${bindings_library} ${wiggly_library}
        LIBRARY DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}
        RUNTIME DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}
        )
install(FILES ${windows_shiboken_shared_libraries} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR})
# =============================================================================================
# !!! End of dubious section.
# =============================================================================================
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

import sysconfig
from enum import Enum
import glob
import os
import re
import sys


PYSIDE = 'pyside6'
PYSIDE_MODULE = 'PySide6'
SHIBOKEN = 'shiboken6'


class Package(Enum):
    SHIBOKEN_MODULE = 1
    SHIBOKEN_GENERATOR = 2
    PYSIDE_MODULE = 3


generic_error = ('Did you forget to activate your virtualenv? Or perhaps'
                 f' you forgot to build / install {PYSIDE_MODULE} into your currently active Python'
                 ' environment?')
pyside_error = f'Unable to locate {PYSIDE_MODULE}. {generic_error}'
shiboken_module_error = f'Unable to locate {SHIBOKEN}-module. {generic_error}'
shiboken_generator_error = f'Unable to locate shiboken-generator. {generic_error}'
pyside_libs_error = f'Unable to locate the PySide shared libraries.  {generic_error}'
python_link_error = 'Unable to locate the Python library for linking.'
python_include_error = 'Unable to locate the Python include headers directory.'

options = []

# option, function, error, description
options.append(("--shiboken-module-path",
                lambda: find_shiboken_module(),
                shiboken_module_error,
                "Print shiboken module location"))
options.append(("--shiboken-generator-path",
                lambda: find_shiboken_generator(),
                shiboken_generator_error,
                "Print shiboken generator location"))
options.append(("--pyside-path", lambda: find_pyside(), pyside_error,
                f"Print {PYSIDE_MODULE} location"))

options.append(("--python-include-path",
                lambda: get_python_include_path(),
                python_include_error,
                "Print Python include path"))
options.append(("--shiboken-generator-include-path",
                lambda: get_package_include_path(Package.SHIBOKEN_GENERATOR),
                pyside_error,
                "Print shiboken generator include paths"))
options.append(("--pyside-include-path",
                lambda: get_package_include_path(Package.PYSIDE_MODULE),
                pyside_error,
                "Print PySide6 include paths"))

options.append(("--python-link-flags-qmake", lambda: python_link_flags_qmake(), python_link_error,
                "Print python link flags for qmake"))
options.append(("--python-link-flags-cmake", lambda: python_link_flags_cmake(), python_link_error,
                "Print python link flags for cmake"))

options.append(("--shiboken-module-qmake-lflags",
                lambda: get_package_qmake_lflags(Package.SHIBOKEN_MODULE), pyside_error,
                "Print shiboken6 shared library link flags for qmake"))
options.append(("--pyside-qmake-lflags",
                lambda: get_package_qmake_lflags(Package.PYSIDE_MODULE), pyside_error,
                "Print PySide6 shared library link flags for qmake"))

options.append(("--shiboken-module-shared-libraries-qmake",
                lambda: get_shared_libraries_qmake(Package.SHIBOKEN_MODULE), pyside_libs_error,
                "Print paths of shiboken shared libraries (.so's, .dylib's, .dll's) for qmake"))
options.append(("--shiboken-module-shared-libraries-cmake",
                lambda: get_shared_libraries_cmake(Package.SHIBOKEN_MODULE), pyside_libs_error,
                "Print paths of shiboken shared libraries (.so's, .dylib's, .dll's) for cmake"))

options.append(("--pyside-shared-libraries-qmake",
                lambda: get_shared_libraries_qmake(Package.PYSIDE_MODULE), pyside_libs_error,
                "Print paths of f{PYSIDE_MODULE} shared libraries (.so's, .dylib's, .dll's) "
                "for qmake"))
options.append(("--pyside-shared-libraries-cmake",
                lambda: get_shared_libraries_cmake(Package.PYSIDE_MODULE), pyside_libs_error,
                f"Print paths of {PYSIDE_MODULE} shared libraries (.so's, .dylib's, .dll's) "
                "for cmake"))

options_usage = ''
for i, (flag, _, _, description) in enumerate(options):
    options_usage += f'    {flag:<45} {description}'
    if i < len(options) - 1:
        options_usage += '\n'

usage = f"""
Utility to determine include/link options of shiboken/PySide and Python for qmake/CMake projects
that would like to embed or build custom shiboken/PySide bindings.

Usage: pyside_config.py [option]
Options:
{options_usage}
    -a                                            Print all options and their values
    --help/-h                                     Print this help
"""

option = sys.argv[1] if len(sys.argv) == 2 else '-a'
if option == '-h' or option == '--help':
    print(usage)
    sys.exit(0)


def clean_path(path):
    return path if sys.platform != 'win32' else path.replace('\\', '/')


def shared_library_suffix():
    if sys.platform == 'win32':
        return 'lib'
    elif sys.platform == 'darwin':
        return 'dylib'
    # Linux
    else:
        return 'so.*'


def import_suffixes():
    import importlib.machinery
    return importlib.machinery.EXTENSION_SUFFIXES


def is_debug():
    debug_suffix = '_d.pyd' if sys.platform == 'win32' else '_d.so'
    return any([s.endswith(debug_suffix) for s in import_suffixes()])


def shared_library_glob_pattern():
    glob = '*.' + shared_library_suffix()
    return glob if sys.platform == 'win32' else 'lib' + glob


def filter_shared_libraries(libs_list):
    def predicate(lib_name):
        basename = os.path.basename(lib_name)
        if 'shiboken' in basename or 'pyside6' in basename:
            return True
        return False
    result = [lib for lib in libs_list if predicate(lib)]
    return result


# Return qmake link option for a library file name
def link_option(lib):
    # On Linux:
    # Since we cannot include symlinks with wheel packages
    # we are using an absolute path for the libpyside and libshiboken
    # libraries when compiling the project
    baseName = os.path.basename(lib)
    link = ' -l'
    if sys.platform in ['linux', 'linux2']:  # Linux: 'libfoo.so' -> '/absolute/path/libfoo.so'
        link = lib
    elif sys.platform in ['darwin']:  # Darwin: 'libfoo.so' -> '-lfoo'
        link += os.path.splitext(baseName[3:])[0]
    else:  # Windows: 'libfoo.dll' -> 'libfoo.dll'
        link += os.path.splitext(baseName)[0]
    return link


# Locate PySide6 via sys.path package path.
def find_pyside():
    return find_package_path(PYSIDE_MODULE)


def find_shiboken_module():
    return find_package_path(SHIBOKEN)


def find_shiboken_generator():
    return find_package_path(f"{SHIBOKEN}_generator")


def find_package(which_package):
    if which_package == Package.SHIBOKEN_MODULE:
        return find_shiboken_module()
    if which_package == Package.SHIBOKEN_GENERATOR:
        return find_shiboken_generator()
    if which_package == Package.PYSIDE_MODULE:
        return find_pyside()
    return None


def find_package_path(dir_name):
    for p in sys.path:
        if 'site-' in p:
            package = os.path.join(p, dir_name)
            if os.path.exists(package):
                return clean_path(os.path.realpath(package))
    return None


# Return version as "x.y" (e.g. 3.9, 3.12, etc)
def python_version():
    return str(sys.version_info[0]) + '.' + str(sys.version_info[1])


def get_python_include_path():
    return sysconfig.get_path('include')


def python_link_flags_qmake():
    flags = python_link_data()
    if sys.platform == 'win32':
        libdir = flags['libdir']
        # This will add the "~1" shortcut for directories that
        # contain white spaces
        # e.g.: "Program Files" to "Progra~1"
        for d in libdir.split("\\"):
            if " " in d:
                libdir = libdir.replace(d, d.split(" ")[0][:-1] + "~1")
        lib_flags = flags['lib']
        return f'-L{libdir} -l{lib_flags}'
    elif sys.platform == 'darwin':
        libdir = flags['libdir']
        lib_flags = flags['lib']
        return f'-L{libdir} -l{lib_flags}'
    else:
        # Linux and anything else
        libdir = flags['libdir']
        lib_flags = flags['lib']
        return f'-L{libdir} -l{lib_flags}'


def python_link_flags_cmake():
    flags = python_link_data()
    libdir = flags['libdir']
    lib = re.sub(r'.dll$', '.lib', flags['lib'])
    return f'{libdir};{lib}'


def python_link_data():
    # @TODO Fix to work with static builds of Python
    libdir = sysconfig.get_config_var('LIBDIR')
    if libdir is None:
        libdir = os.path.abspath(os.path.join(
            sysconfig.get_config_var('LIBDEST'), "..", "libs"))
    version = python_version()
    version_no_dots = version.replace('.', '')

    flags = {}
    flags['libdir'] = libdir
    if sys.platform == 'win32':
        suffix = '_d' if is_debug() else ''
        flags['lib'] = f'python{version_no_dots}{suffix}'

    elif sys.platform == 'darwin':
        flags['lib'] = f'python{version}'

    # Linux and anything else
    else:
        flags['lib'] = f'python{version}{sys.abiflags}'

    return flags


def get_package_include_path(which_package):
    package_path = find_package(which_package)
    if package_path is None:
        return None

    includes = f"{package_path}/include"

    return includes


def get_package_qmake_lflags(which_package):
    package_path = find_package(which_package)
    if package_path is None:
        return None

    link = f"-L{package_path}"
    glob_result = glob.glob(os.path.join(package_path, shared_library_glob_pattern()))
    for lib in filter_shared_libraries(glob_result):
        link += ' '
        link += link_option(lib)
    return link


def get_shared_libraries_data(which_package):
    package_path = find_package(which_package)
    if package_path is None:
        return None

    glob_result = glob.glob(os.path.join(package_path, shared_library_glob_pattern()))
    filtered_libs = filter_shared_libraries(glob_result)
    libs = []
    if sys.platform == 'win32':
        for lib in filtered_libs:
            libs.append(os.path.realpath(lib))
    else:
        for lib in filtered_libs:
            libs.append(lib)
    return libs


def get_shared_libraries_qmake(which_package):
    libs = get_shared_libraries_data(which_package)
    if libs is None:
        return None

    if sys.platform == 'win32':
        if not libs:
            return ''
        dlls = ''
        for lib in libs:
            dll = os.path.splitext(lib)[0] + '.dll'
            dlls += dll + ' '

        return dlls
    else:
        libs_string = ''
        for lib in libs:
            libs_string += lib + ' '
        return libs_string


def get_shared_libraries_cmake(which_package):
    libs = get_shared_libraries_data(which_package)
    result = ';'.join(libs)
    return result


print_all = option == "-a"
for argument, handler, error, _ in options:
    if option == argument or print_all:
        handler_result = handler()
        if handler_result is None:
            sys.exit(error)

        line = handler_result
        if print_all:
            line = f"{argument:<40}: {line}"
        print(line)