diff --git a/CMakeLists.txt b/CMakeLists.txt
index 919969e..46e20e6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -3,14 +3,22 @@ project(python_qt_binding)
find_package(ament_cmake REQUIRED)
find_package(ament_cmake_python REQUIRED)
+find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
-ament_python_install_package(${PROJECT_NAME}
- PACKAGE_DIR src/${PROJECT_NAME})
+if (${QT_VERSION_MAJOR} GREATER "5")
+ ament_python_install_package(python_qt_binding
+ PACKAGE_DIR src/python_qt6_binding)
+else()
+ ament_python_install_package(python_qt_binding
+ PACKAGE_DIR src/python_qt_binding
+ )
+endif()
install(FILES
cmake/shiboken_helper.cmake
cmake/sip_configure.py
cmake/sip_helper.cmake
+ cmake/pyside_config.py
DESTINATION share/${PROJECT_NAME}/cmake)
if(BUILD_TESTING)
diff --git a/cmake/pyside_config.py b/cmake/pyside_config.py
new file mode 100755
index 0000000..2282e43
--- /dev/null
+++ b/cmake/pyside_config.py
@@ -0,0 +1,341 @@
+#!/usr/bin/python3
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from enum import Enum
+from glob import glob
+import os
+import re
+import sys
+import sysconfig
+
+
+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 or 'dist-' 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(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(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)
diff --git a/cmake/shiboken_helper.cmake b/cmake/shiboken_helper.cmake
index 7624157..008ec9f 100644
--- a/cmake/shiboken_helper.cmake
+++ b/cmake/shiboken_helper.cmake
@@ -162,4 +162,4 @@ function(shiboken_target_link_libraries PROJECT_NAME QT_COMPONENTS)
endforeach()
target_link_libraries(${PROJECT_NAME} ${shiboken_LINK_LIBRARIES})
-endfunction()
+endfunction()
\ No newline at end of file
diff --git a/cmake/sip_configure.py b/cmake/sip_configure.py
index 5210ee5..a25c832 100644
--- a/cmake/sip_configure.py
+++ b/cmake/sip_configure.py
@@ -228,4 +228,4 @@ def split_paths(paths):
makefile.LIBS.set(libs)
# Generate the Makefile itself
-makefile.generate()
+makefile.generate()
\ No newline at end of file
diff --git a/cmake/sip_helper.cmake b/cmake/sip_helper.cmake
index a5ac3c2..7cf6d68 100644
--- a/cmake/sip_helper.cmake
+++ b/cmake/sip_helper.cmake
@@ -31,9 +31,11 @@ execute_process(
if(PYTHON_SIP_EXECUTABLE)
string(STRIP ${PYTHON_SIP_EXECUTABLE} SIP_EXECUTABLE)
else()
- find_program(SIP_EXECUTABLE sip)
+ find_program(SIP_EXECUTABLE NAMES sip-build)
endif()
+set(SIP_PROJECT_INCLUDE_DIRS "$ENV{SIP_PROJECT_INCLUDE_DIRS}")
+
if(SIP_EXECUTABLE)
message(STATUS "SIP binding generator available at: ${SIP_EXECUTABLE}")
set(sip_helper_FOUND TRUE)
@@ -42,6 +44,138 @@ else()
set(sip_helper_NOTFOUND TRUE)
endif()
+if(sip_helper_FOUND)
+ execute_process(
+ COMMAND ${SIP_EXECUTABLE} -V
+ OUTPUT_VARIABLE SIP_VERSION
+ ERROR_QUIET)
+ string(STRIP ${SIP_VERSION} SIP_VERSION)
+ message(STATUS "SIP binding generator version: ${SIP_VERSION}")
+endif()
+
+execute_process(
+ COMMAND ${Python3_EXECUTABLE} -c "import sysconfig as c; print(c.get_config_var('EXT_SUFFIX'), end='')"
+ OUTPUT_VARIABLE PYTHON_EXTENSION_MODULE_SUFFIX
+ ERROR_QUIET)
+
+#
+# Run the SIP generator and compile the generated code into a library.
+#
+# .. note:: The target lib${PROJECT_NAME} is created.
+#
+# :param PROJECT_NAME: The name of the sip project
+# :type PROJECT_NAME: string
+# :param SIP_FILE: the SIP file to be processed
+# :type SIP_FILE: string
+#
+# The following options can be used to override the default behavior:
+# SIP_CONFIGURE: the used configure script for SIP
+# (default: sip_configure.py in the same folder as this file)
+# SOURCE_DIR: the source dir (default: ${PROJECT_SOURCE_DIR}/src)
+# LIBRARY_DIR: the library dir (default: ${PROJECT_SOURCE_DIR}/src)
+# BINARY_DIR: the binary dir (default: ${PROJECT_BINARY_DIR})
+#
+# The following keywords arguments can be used to specify:
+# DEPENDS: depends for the custom command
+# (should list all sip and header files)
+# DEPENDENCIES: target dependencies
+# (should list the library for which SIP generates the bindings)
+#
+function(build_sip_6_binding PROJECT_NAME SIP_FILE)
+ cmake_parse_arguments(sip "" "SIP_CONFIGURE;SOURCE_DIR;LIBRARY_DIR;BINARY_DIR" "DEPENDS;DEPENDENCIES" ${ARGN})
+ if(sip_UNPARSED_ARGUMENTS)
+ message(WARNING "build_sip_binding(${PROJECT_NAME}) called with unused arguments: ${sip_UNPARSED_ARGUMENTS}")
+ endif()
+
+ # set default values for optional arguments
+ if(NOT sip_SIP_CONFIGURE)
+ # default to sip_configure.py in this directory
+ set(sip_SIP_CONFIGURE ${__PYTHON_QT_BINDING_SIP_HELPER_DIR}/sip_configure.py)
+ endif()
+ if(NOT sip_SOURCE_DIR)
+ set(sip_SOURCE_DIR ${PROJECT_SOURCE_DIR}/src)
+ endif()
+ if(NOT sip_LIBRARY_DIR)
+ set(sip_LIBRARY_DIR ${PROJECT_SOURCE_DIR}/lib)
+ endif()
+ if(NOT sip_BINARY_DIR)
+ set(sip_BINARY_DIR ${PROJECT_BINARY_DIR})
+ endif()
+
+ set(SIP_BUILD_DIR ${sip_BINARY_DIR}/sip/${PROJECT_NAME})
+
+ set(INCLUDE_DIRS ${${PROJECT_NAME}_INCLUDE_DIRS} ${Python3_INCLUDE_DIRS})
+ set(LIBRARIES ${${PROJECT_NAME}_LIBRARIES})
+ set(LIBRARY_DIRS ${${PROJECT_NAME}_LIBRARY_DIRS})
+ set(LDFLAGS_OTHER ${${PROJECT_NAME}_LDFLAGS_OTHER})
+
+ find_program(QMAKE_EXECUTABLE NAMES qmake REQUIRED)
+
+ file(REMOVE_RECURSE ${SIP_BUILD_DIR})
+ file(MAKE_DIRECTORY ${sip_LIBRARY_DIR})
+ set(SIP_FILES_DIR ${sip_SOURCE_DIR})
+
+ set(SIP_INCLUDE_DIRS "")
+ foreach(_x ${INCLUDE_DIRS})
+ set(SIP_INCLUDE_DIRS "${SIP_INCLUDE_DIRS},\"${_x}\"")
+ endforeach()
+ string(REGEX REPLACE "^," "" SIP_INCLUDE_DIRS "${SIP_INCLUDE_DIRS}")
+
+ # pyproject.toml expects libraries listed as such to be added to the linker command
+ # via `-l`, but this does not work for libraries with absolute paths
+ # instead we have to pass them to the linker via a different parameter
+ set(_SIP_ABS_LIBRARIES "")
+ foreach(_x ${LIBRARIES} ${Python3_LIBRARIES})
+ cmake_path(IS_ABSOLUTE _x is_abs)
+ if(is_abs)
+ list(APPEND _SIP_ABS_LIBRARIES "${_x}")
+ endif()
+ endforeach()
+
+ if(APPLE)
+ set(LIBQT_GUI_CPP_SIP_SUFFIX .so)
+ elseif(WIN32)
+ set(LIBQT_GUI_CPP_SIP_SUFFIX .pyd)
+ else()
+ set(LIBQT_GUI_CPP_SIP_SUFFIX ${CMAKE_SHARED_LIBRARY_SUFFIX})
+ endif()
+
+ list(APPEND _SIP_ABS_LIBRARIES ${sip_BINARY_DIR}/${sip_DEPENDENCIES}${LIBQT_GUI_CPP_SIP_SUFFIX})
+ list(JOIN _SIP_ABS_LIBRARIES " " SIP_ABS_LIBRARIES)
+
+ set(SIP_LIBRARY_DIRS "")
+ foreach(_x ${LIBRARY_DIRS})
+ set(SIP_LIBRARY_DIRS "${SIP_LIBRARY_DIRS},\"${_x}\"")
+ endforeach()
+ string(REGEX REPLACE "^," "" SIP_LIBRARY_DIRS "${SIP_LIBRARY_DIRS}")
+
+ set(SIP_EXTRA_DEFINES "")
+ foreach(_x ${EXTRA_DEFINES})
+ set(SIP_EXTRA_DEFINES "${SIP_EXTRA_DEFINES},\"${_x}\"")
+ endforeach()
+ string(REGEX REPLACE "^," "" SIP_EXTRA_DEFINES "${SIP_EXTRA_DEFINES}")
+
+ set(SIP_PROJECT_INCLUDE_DIRS /home/ahcorde/ros2_rolling/qt6-env/lib/python3.12/site-packages/PyQt6/bindings)
+
+ configure_file(
+ ${sip_SOURCE_DIR}/pyproject.toml.in
+ ${sip_BINARY_DIR}/sip/pyproject.toml
+ )
+ add_custom_command(
+ OUTPUT ${sip_LIBRARY_DIR}/lib${PROJECT_NAME}${PYTHON_EXTENSION_MODULE_SUFFIX}
+ COMMAND ${Python3_EXECUTABLE} -m pip install . --target ${sip_LIBRARY_DIR} --no-deps --no-build-isolation
+ DEPENDS ${sip_SIP_CONFIGURE} ${SIP_FILE} ${sip_DEPENDS}
+ WORKING_DIRECTORY ${sip_BINARY_DIR}/sip
+ COMMENT "Running SIP-build generator for ${PROJECT_NAME} Python bindings..."
+ )
+
+ add_custom_target(lib${PROJECT_NAME} ALL
+ DEPENDS ${sip_LIBRARY_DIR}/lib${PROJECT_NAME}${PYTHON_EXTENSION_MODULE_SUFFIX}
+ COMMENT "Meta target for ${PROJECT_NAME} Python bindings..."
+ )
+ add_dependencies(lib${PROJECT_NAME} ${sip_DEPENDENCIES})
+endfunction()
+
#
# Run the SIP generator and compile the generated code into a library.
#
@@ -95,8 +229,7 @@ function(build_sip_binding PROJECT_NAME SIP_FILE)
add_custom_command(
OUTPUT ${SIP_BUILD_DIR}/Makefile
- COMMAND ${Python3_EXECUTABLE} ${sip_SIP_CONFIGURE} ${SIP_BUILD_DIR} ${SIP_FILE} ${sip_LIBRARY_DIR}
- \"${INCLUDE_DIRS}\" \"${LIBRARIES}\" \"${LIBRARY_DIRS}\" \"${LDFLAGS_OTHER}\"
+ COMMAND ${Python3_EXECUTABLE} ${sip_SIP_CONFIGURE} ${SIP_BUILD_DIR} ${SIP_FILE} ${sip_LIBRARY_DIR} \"${INCLUDE_DIRS}\" \"${LIBRARIES}\" \"${LIBRARY_DIRS}\" \"${LDFLAGS_OTHER}\" \"${EXTRA_DEFINES}\"
DEPENDS ${sip_SIP_CONFIGURE} ${SIP_FILE} ${sip_DEPENDS}
WORKING_DIRECTORY ${sip_SOURCE_DIR}
COMMENT "Running SIP generator for ${PROJECT_NAME} Python bindings..."
diff --git a/package.xml b/package.xml
index 34703d6..7a454d2 100644
--- a/package.xml
+++ b/package.xml
@@ -28,7 +28,7 @@
ament_cmake
- qtbase5-dev
+ qt6-base-dev
python3-qt5-bindings
python3-qt5-bindings
@@ -36,7 +36,7 @@
ament_cmake_pytest
ament_lint_auto
ament_lint_common
-
+
ament_cmake
diff --git a/src/python_qt6_binding/__init__.py b/src/python_qt6_binding/__init__.py
new file mode 100644
index 0000000..59a8769
--- /dev/null
+++ b/src/python_qt6_binding/__init__.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2011, Dirk Thomas, Dorian Scholz, TU Darmstadt
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of the TU Darmstadt nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+"""
+Abstraction for different Python Qt bindings.
+
+Supported Python Qt 5 bindings are PyQt and PySide.
+The Qt modules can be imported like this:
+
+from python_qt_binding.QtCore import QObject
+from python_qt_binding import QtGui, loadUi
+
+The name of the selected binding is available in QT_BINDING.
+The version of the selected binding is available in QT_BINDING_VERSION.
+All available Qt modules are listed in QT_BINDING_MODULES.
+
+The default binding order ('pyqt', 'pyside') can be overridden with a
+SELECT_QT_BINDING_ORDER attribute on sys:
+ setattr(sys, 'SELECT_QT_BINDING_ORDER', [FIRST_NAME, NEXT_NAME, ..])
+
+A specific binding can be selected with a SELECT_QT_BINDING attribute on sys:
+ setattr(sys, 'SELECT_QT_BINDING', MY_BINDING_NAME)
+"""
+
+import sys
+
+from python_qt_binding.binding_helper import loadUi # noqa: F401
+from python_qt_binding.binding_helper import QT_BINDING # noqa: F401
+from python_qt_binding.binding_helper import QT_BINDING_MODULES
+from python_qt_binding.binding_helper import QT_BINDING_VERSION # noqa: F401
+
+print('QT_BINDING', QT_BINDING)
+for module, value in QT_BINDING_MODULES.items():
+ print('QT_BINDING_MODULES', module, value)
+print('QT_BINDING_VERSION', QT_BINDING_VERSION)
+
+# register binding modules as sub modules of this package (python_qt_binding) for easy importing
+for module_name, module in QT_BINDING_MODULES.items():
+ sys.modules[__name__ + '.' + module_name] = module
+ setattr(sys.modules[__name__], module_name, module)
+ del module_name
+ del module
+
+del sys
diff --git a/src/python_qt6_binding/binding_helper.py b/src/python_qt6_binding/binding_helper.py
new file mode 100644
index 0000000..bd5bca5
--- /dev/null
+++ b/src/python_qt6_binding/binding_helper.py
@@ -0,0 +1,287 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2011, Dirk Thomas, Dorian Scholz, TU Darmstadt
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of the TU Darmstadt nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+try:
+ import __builtin__ as builtins
+except ImportError:
+ # since the 'future' package provides a 'builtins' module in Python 2
+ # this must not be checked second
+ import builtins
+import os
+import platform
+import sys
+import traceback
+
+
+QT_BINDING = None
+QT_BINDING_MODULES = {}
+QT_BINDING_VERSION = None
+
+
+def _select_qt_binding(binding_name=None, binding_order=None):
+ global QT_BINDING, QT_BINDING_VERSION
+
+ # order of default bindings can be changed here
+ DEFAULT_BINDING_ORDER = ['pyqt']
+ # if platform.system() == 'Darwin':
+ # DEFAULT_BINDING_ORDER = ['pyside']
+ # else:
+ # DEFAULT_BINDING_ORDER = ['pyside', 'pyqt']
+
+ binding_order = binding_order or DEFAULT_BINDING_ORDER
+
+ # determine binding preference
+ if binding_name:
+ if binding_name not in binding_order:
+ raise ImportError("Qt binding '%s' is unknown" % binding_name)
+ binding_order = [binding_name]
+
+ required_modules = [
+ 'QtCore',
+ 'QtGui',
+ 'QtWidgets',
+ ]
+ optional_modules = [
+ 'QtBluetooth',
+ 'QtDBus',
+ 'QtDesigner',
+ 'QtHelp',
+ 'QtLocation',
+ 'QtMultimedia',
+ 'QtMultimediaWidgets',
+ 'QtNetwork',
+ 'QNetworkAuth',
+ 'QtNfc',
+ 'QtOpenGL',
+ 'QtPositioning',
+ 'QtPrintSupport',
+ 'QtQml',
+ 'QtQuick',
+ 'QtQuickWidgets',
+ 'QtScript',
+ 'QtScriptTools',
+ 'QtSensors',
+ 'QtSerialPort',
+ 'QtSql',
+ 'QtSvg',
+ 'QtTest',
+ 'QtWebChannel',
+ 'QtWebEngine', # Qt 5.6 and higher
+ 'QtWebEngineCore',
+ 'QtWebEngineWidgets',
+ 'QtWebKitWidgets', # Qt 5.0 - 5.5
+ 'QtWebSockets',
+ 'QtX11Extras',
+ 'QtXml',
+ 'QtXmlPatterns',
+ ]
+
+ # try to load preferred bindings
+ error_msgs = []
+ for binding_name in binding_order:
+ try:
+ binding_loader = getattr(sys.modules[__name__], '_load_%s' % binding_name, None)
+ if binding_loader:
+ QT_BINDING_VERSION = binding_loader(required_modules, optional_modules)
+ QT_BINDING = binding_name
+ break
+ else:
+ error_msgs.append(" Binding loader '_load_%s' not found." % binding_name)
+ except ImportError as e:
+ error_msgs.append(" ImportError for '%s': %s\n%s" %
+ (binding_name, e, traceback.format_exc()))
+
+ if not QT_BINDING:
+ raise ImportError(
+ 'Could not find Qt binding (looked for: %s):\n%s' %
+ (', '.join(["'%s'" % b for b in binding_order]), '\n'.join(error_msgs)))
+
+
+def _register_binding_module(module_name, module):
+ # register module using only its own name (TODO: legacy compatibility, remove when possible)
+ sys.modules[module_name] = module
+ # add module to the binding modules
+ QT_BINDING_MODULES[module_name] = module
+
+
+def _named_import(name):
+ parts = name.split('.')
+ assert len(parts) >= 2
+ module = builtins.__import__(name)
+ for m in parts[1:]:
+ module = module.__dict__[m]
+ module_name = parts[-1]
+ _register_binding_module(module_name, module)
+
+
+def _named_optional_import(name):
+ try:
+ _named_import(name)
+ except ImportError:
+ pass
+
+
+def _load_pyqt(required_modules, optional_modules):
+ # set environment variable QT_API for matplotlib
+ os.environ['QT_API'] = 'pyqt'
+
+ # register required and optional PyQt modules
+ for module_name in required_modules:
+ print('PyQt6.%s' % module_name)
+ _named_import('PyQt6.%s' % module_name)
+ for module_name in optional_modules:
+ _named_optional_import('PyQt6.%s' % module_name)
+
+ # set some names for compatibility with PySide
+ sys.modules['QtCore'].Signal = sys.modules['QtCore'].pyqtSignal
+ sys.modules['QtCore'].Slot = sys.modules['QtCore'].pyqtSlot
+ sys.modules['QtCore'].Property = sys.modules['QtCore'].pyqtProperty
+
+ # try to register Qwt module
+ try:
+ import PyQt6.Qwt6
+ _register_binding_module('Qwt', PyQt6.Qwt6)
+ except ImportError:
+ pass
+
+ global _loadUi
+
+ def _loadUi(uifile, baseinstance=None, custom_widgets_=None):
+ from PyQt6 import uic
+ return uic.loadUi(uifile, baseinstance=baseinstance)
+
+ import PyQt6.QtCore
+ return PyQt6.QtCore.PYQT_VERSION_STR
+
+
+def _load_pyside(required_modules, optional_modules):
+ # set environment variable QT_API for matplotlib
+ os.environ['QT_API'] = 'pyside'
+
+ # register required and optional PySide modules
+ for module_name in required_modules:
+ _named_import('PySide6.%s' % module_name)
+ for module_name in optional_modules:
+ _named_optional_import('PySide6.%s' % module_name)
+
+ # set some names for compatibility with PyQt
+ sys.modules['QtCore'].pyqtSignal = sys.modules['QtCore'].Signal
+ sys.modules['QtCore'].pyqtSlot = sys.modules['QtCore'].Slot
+ sys.modules['QtCore'].pyqtProperty = sys.modules['QtCore'].Property
+
+ # try to register PySideQwt module
+ try:
+ import PySideQwt
+ _register_binding_module('Qwt', PySideQwt)
+ except ImportError:
+ pass
+
+ global _loadUi
+
+ def _loadUi(uifile, baseinstance=None, custom_widgets=None):
+ from PySide6.QtUiTools import QUiLoader
+ from PySide6.QtCore import QMetaObject
+
+ class CustomUiLoader(QUiLoader):
+ class_aliases = {
+ 'Line': 'QFrame',
+ }
+
+ def __init__(self, baseinstance=None, custom_widgets=None):
+ super(CustomUiLoader, self).__init__(baseinstance)
+ self._base_instance = baseinstance
+ self._custom_widgets = custom_widgets or {}
+
+ def createWidget(self, class_name, parent=None, name=''):
+ # don't create the top-level widget, if a base instance is set
+ if self._base_instance and not parent:
+ return self._base_instance
+
+ if class_name in self._custom_widgets:
+ widget = self._custom_widgets[class_name](parent)
+ else:
+ widget = QUiLoader.createWidget(self, class_name, parent, name)
+
+ if str(type(widget)).find(self.class_aliases.get(class_name, class_name)) < 0:
+ sys.modules['QtCore'].qDebug(
+ 'PySide.loadUi(): could not find widget class "%s", defaulting to "%s"' %
+ (class_name, type(widget)))
+
+ if self._base_instance:
+ setattr(self._base_instance, name, widget)
+
+ return widget
+
+ loader = CustomUiLoader(baseinstance, custom_widgets)
+
+ # instead of passing the custom widgets, they should be registered using
+ # QUiLoader.registerCustomWidget(),
+ # but this does not work in PySide 1.0.6: it simply segfaults...
+ # loader = CustomUiLoader(baseinstance)
+ # custom_widgets = custom_widgets or {}
+ # for custom_widget in custom_widgets.values():
+ # loader.registerCustomWidget(custom_widget)
+
+ ui = loader.load(uifile)
+ QMetaObject.connectSlotsByName(ui)
+ return ui
+
+ import PySide6
+ return PySide6.__version__
+
+
+def loadUi(uifile, baseinstance=None, custom_widgets=None):
+ """
+ Load a provided UI file chosen Python Qt 5 binding.
+
+ @type uifile: str
+ @param uifile: Absolute path of .ui file
+ @type baseinstance: QWidget
+ @param baseinstance: the optional instance of the Qt base class.
+ If specified then the user interface is created in
+ it. Otherwise a new instance of the base class is
+ automatically created.
+ @type custom_widgets: dict of {str:QWidget}
+ @param custom_widgets: Class name and type of the custom classes used
+ in uifile if any. This can be None if no custom
+ class is in use. (Note: this is only necessary
+ for PySide, see
+ http://answers.ros.org/question/56382/what-does-python_qt_bindingloaduis-3rd-arg-do-in-pyqt-binding/
+ for more information)
+ """
+ return _loadUi(uifile, baseinstance, custom_widgets)
+
+
+_select_qt_binding(
+ getattr(sys, 'SELECT_QT_BINDING', None),
+ getattr(sys, 'SELECT_QT_BINDING_ORDER', None),
+)
diff --git a/src/python_qt_binding/__init__.py b/src/python_qt_binding/__init__.py
index 1e209de..59a8769 100644
--- a/src/python_qt_binding/__init__.py
+++ b/src/python_qt_binding/__init__.py
@@ -58,6 +58,11 @@
from python_qt_binding.binding_helper import QT_BINDING_MODULES
from python_qt_binding.binding_helper import QT_BINDING_VERSION # noqa: F401
+print('QT_BINDING', QT_BINDING)
+for module, value in QT_BINDING_MODULES.items():
+ print('QT_BINDING_MODULES', module, value)
+print('QT_BINDING_VERSION', QT_BINDING_VERSION)
+
# register binding modules as sub modules of this package (python_qt_binding) for easy importing
for module_name, module in QT_BINDING_MODULES.items():
sys.modules[__name__ + '.' + module_name] = module
diff --git a/src/python_qt_binding/binding_helper.py b/src/python_qt_binding/binding_helper.py
index 27c3237..63ccf92 100644
--- a/src/python_qt_binding/binding_helper.py
+++ b/src/python_qt_binding/binding_helper.py
@@ -282,4 +282,4 @@ class is in use. (Note: this is only necessary
_select_qt_binding(
getattr(sys, 'SELECT_QT_BINDING', None),
getattr(sys, 'SELECT_QT_BINDING_ORDER', None),
-)
+)
\ No newline at end of file