diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c6c536c..503daffe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,7 @@ cmake_policy(SET CMP0020 NEW) cmake_policy(SET CMP0042 NEW) option(OPTION_DEVELOPER_MODE "Developer Mode" OFF) +option(OPTION_BUILD_PYTHON_BINDINGS "Build python bindings" OFF) # option(OPTION_QTQUICK "Build for QtQuick instead of QtWidgets" OFF) find_package(Qt5Widgets) @@ -22,7 +23,8 @@ find_package(Qt5Widgets) set(CMAKE_AUTOMOC ON) set(ECM_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cmake/ECM/modules/") -set(CMAKE_MODULE_PATH ${ECM_MODULE_DIR}) +set(PYTHON_MODULE_DIR "${CMAKE_CURRENT_LIST_DIR}/cmake/Python") +set(CMAKE_MODULE_PATH ${ECM_MODULE_DIR} ${PYTHON_MODULE_DIR}) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) if (OPTION_DEVELOPER_MODE) @@ -42,6 +44,9 @@ endif() add_subdirectory(src) add_subdirectory(examples/dockwidgets) +if (OPTION_BUILD_PYTHON_BINDINGS) + add_subdirectory(python) +endif() if (OPTION_DEVELOPER_MODE) if (NOT OPTION_QTQUICK) diff --git a/cmake/Python/FindPySide2.cmake b/cmake/Python/FindPySide2.cmake new file mode 100644 index 00000000..c117932c --- /dev/null +++ b/cmake/Python/FindPySide2.cmake @@ -0,0 +1,143 @@ +### +# This file is part of KDDockWidgets. +# +# Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +# Author: Renato Araujo Oliveira Filho +# +# This program is free software you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +## + +# PYSIDE_BASEDIR - Top of the PySide2 installation +# PYSIDE_INCLUDE_DIR - Directories to include to use PySide2 +# PYSIDE_LIBRARY - Files to link against to use PySide2 +# PYSIDE_TYPESYSTEMS - Type system files that should be used by other bindings extending PySide2 +# +# You can install PySide2 from Qt repository with +# pip3 install --index-url=http://download.qt.io/snapshots/ci/pyside//latest/ pyside2 --trusted-host download.qt.io + +set(PYSIDE2_FOUND FALSE) +set(PYSIDE2_CUSTOM_PREFIX "/usr/" CACHE PATH "Extra path to look for PySide components.") + +# extract python library basename +list(GET Python3_LIBRARIES 0 PYTHON_LIBRARY_FILENAME) +get_filename_component(PYTHON_LIBRARY_FILENAME ${PYTHON_LIBRARY_FILENAME} NAME) + +execute_process( + COMMAND ${Python3_EXECUTABLE} -c "if True: + import os, sys + try: + import PySide2.QtCore as QtCore + print(os.path.dirname(QtCore.__file__)) + except Exception as error: + print(error, file=sys.stderr) + exit() + " + OUTPUT_VARIABLE PYSIDE2_BASEDIR + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +if(PYSIDE2_BASEDIR) + set(PYSIDE_BASEDIR ${PYSIDE2_BASEDIR} CACHE PATH "Top level install of PySide2" FORCE) + execute_process( + COMMAND ${Python3_EXECUTABLE} -c "if True: + import os + import PySide2.QtCore as QtCore + print(os.path.basename(QtCore.__file__).split('.', 1)[1]) + " + OUTPUT_VARIABLE PYSIDE2_SUFFIX + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + execute_process( + COMMAND ${Python3_EXECUTABLE} -c "if True: + import os + import PySide2.QtCore as QtCore + print(';'.join(map(str, QtCore.__version_info__))) + " + OUTPUT_VARIABLE PYSIDE2_SO_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + list(GET PYSIDE2_SO_VERSION 0 PYSIDE2_SO_MACRO_VERSION) + list(GET PYSIDE2_SO_VERSION 1 PYSIDE2_SO_MICRO_VERSION) + list(GET PYSIDE2_SO_VERSION 2 PYSIDE2_SO_MINOR_VERSION) + string(REPLACE ";" "." PYSIDE2_SO_VERSION "${PYSIDE2_SO_VERSION}") + + if(NOT APPLE) + set(PYSIDE2_SUFFIX "${PYSIDE2_SUFFIX}.${PYSIDE2_SO_MACRO_VERSION}.${PYSIDE2_SO_MICRO_VERSION}") + else() + string(REPLACE ".so" "" PYSIDE2_SUFFIX ${PYSIDE2_SUFFIX}) + set(PYSIDE2_SUFFIX "${PYSIDE2_SUFFIX}.${PYSIDE2_SO_MACRO_VERSION}.${PYSIDE2_SO_MICRO_VERSION}.dylib") + endif() + + set(PYSIDE2_FOUND TRUE) + message(STATUS "PySide2 base dir: ${PYSIDE2_BASEDIR}" ) + message(STATUS "PySide2 version: ${PYSIDE2_SO_VERSION}") + message(STATUS "PySide2 suffix: ${PYSIDE2_SUFFIX}") +endif() + +if (PYSIDE2_FOUND) + #PySide + #=============================================================================== + find_path(PYSIDE_INCLUDE_DIR + pyside.h + PATHS ${PYSIDE2_BASEDIR}/include ${PYSIDE2_CUSTOM_PREFIX}/include/PySide2 + NO_DEFAULT_PATH) + + # Platform specific library names + if(MSVC) + SET(PYSIDE_LIBRARY_BASENAMES "pyside2.abi3.lib") + elseif(CYGWIN) + SET(PYSIDE_LIBRARY_BASENAMES "") + elseif(WIN32) + SET(PYSIDE_LIBRARY_BASENAMES "libpyside2.${PYSIDE2_SUFFIX}") + else() + SET(PYSIDE_LIBRARY_BASENAMES "libpyside2.${PYSIDE2_SUFFIX}") + endif() + + find_file(PYSIDE_LIBRARY + ${PYSIDE_LIBRARY_BASENAMES} + PATHS ${PYSIDE2_BASEDIR} ${PYSIDE2_CUSTOM_PREFIX}/lib + NO_DEFAULT_PATH) + + find_path(PYSIDE_TYPESYSTEMS + typesystem_core.xml + PATHS ${PYSIDE2_BASEDIR}/typesystems ${PYSIDE2_CUSTOM_PREFIX}/share/PySide2/typesystems + NO_DEFAULT_PATH) + + message(STATUS "PySide include dir: ${PYSIDE_INCLUDE_DIR}") + message(STATUS "PySide library: ${PYSIDE_LIBRARY}") + message(STATUS "PySide typesystems: ${PYSIDE_TYPESYSTEMS}") + + # Create PySide2 target + add_library(PySide2::pyside2 SHARED IMPORTED GLOBAL) + if(MSVC) + set_property(TARGET PySide2::pyside2 PROPERTY + IMPORTED_IMPLIB ${PYSIDE_LIBRARY}) + endif() + set_property(TARGET PySide2::pyside2 PROPERTY + IMPORTED_LOCATION ${PYSIDE_LIBRARY}) + set_property(TARGET PySide2::pyside2 APPEND PROPERTY + INTERFACE_INCLUDE_DIRECTORIES + ${PYSIDE_INCLUDE_DIR} + ${PYSIDE_INCLUDE_DIR}/QtCore/ + ${PYSIDE_INCLUDE_DIR}/QtGui/ + ${PYSIDE_INCLUDE_DIR}/QtWidgets/ + ${Python3_INCLUDE_DIRS} + ) +endif() + +find_package_handle_standard_args(PySide2 + REQUIRED_VARS PYSIDE2_BASEDIR PYSIDE_INCLUDE_DIR PYSIDE_LIBRARY PYSIDE_TYPESYSTEMS + VERSION_VAR PYSIDE2_SO_VERSION +) diff --git a/cmake/Python/FindShiboken2.cmake b/cmake/Python/FindShiboken2.cmake new file mode 100644 index 00000000..ce3de4c3 --- /dev/null +++ b/cmake/Python/FindShiboken2.cmake @@ -0,0 +1,158 @@ +### +# This file is part of KDDockWidgets. +# +# Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +# Author: Renato Araujo Oliveira Filho +# +# This program is free software you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +## + +# SHIBOKEN_INCLUDE_DIR - Directories to include to use SHIBOKEN +# SHIBOKEN_LIBRARY - Files to link against to use SHIBOKEN +# SHIBOKEN_BINARY - Executable name +# SHIBOKEN_BUILD_TYPE - Tells if Shiboken was compiled in Release or Debug mode. + +# You can install Shiboken from Qt repository with +# pip3 install --index-url=http://download.qt.io/snapshots/ci/pyside//latest/ shiboken2-generator --trusted-host download.qt.io +set(SHIBOKEN_FOUND FALSE) + +execute_process( + COMMAND ${Python3_EXECUTABLE} -c "if True: + import os + try: + import shiboken2_generator + print(shiboken2_generator.__path__[0]) + except: + exit() + " + OUTPUT_VARIABLE SHIBOKEN_GENERATOR_BASEDIR + OUTPUT_STRIP_TRAILING_WHITESPACE +) +execute_process( + COMMAND ${Python3_EXECUTABLE} -c "if True: + import os + try: + import shiboken2 + print(shiboken2.__path__[0]) + except: + exit() + " + OUTPUT_VARIABLE SHIBOKEN_BASEDIR + OUTPUT_STRIP_TRAILING_WHITESPACE +) +execute_process( + COMMAND ${Python3_EXECUTABLE} -c "if True: + import os + import shiboken2 + print(';'.join(filter(None, map(str, shiboken2.__version_info__)))) + " + OUTPUT_VARIABLE SHIBOKEN_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE +) +list(GET SHIBOKEN_VERSION 0 SHIBOKEN_MACRO_VERSION) +list(GET SHIBOKEN_VERSION 1 SHIBOKEN_MICRO_VERSION) +list(GET SHIBOKEN_VERSION 2 SHIBOKEN_MINOR_VERSION) +string(REPLACE ";" "." SHIBOKEN_VERSION "${SHIBOKEN_VERSION}") + +message(STATUS "ShibokenGenerator base dir: ${SHIBOKEN_GENERATOR_BASEDIR}") +message(STATUS "Shiboken base dir: ${SHIBOKEN_BASEDIR}") +message(STATUS "Shiboken version: ${SHIBOKEN_VERSION}") + +if(SHIBOKEN_BASEDIR) + find_path(SHIBOKEN_INCLUDE_DIR + shiboken.h + PATHS ${SHIBOKEN_GENERATOR_BASEDIR}/include + NO_DEFAULT_PATH) + if(MSVC) + SET(SHIBOKEN_LIBRARY_BASENAMES "shiboken2.abi3.lib") + elseif(CYGWIN) + SET(SHIBOKEN_LIBRARY_BASENAMES "") + elseif(WIN32) + SET(SHIBOKEN_LIBRARY_BASENAMES "libshiboken2.${PYSIDE2_SUFFIX}") + else() + SET(SHIBOKEN_LIBRARY_BASENAMES + libshiboken2.abi3.so + libshiboken2.abi3.so.${SHIBOKEN_MACRO_VERSION} + libshiboken2.abi3.so.${SHIBOKEN_MACRO_VERSION}.${SHIBOKEN_MICRO_VERSION} + libshiboken2.abi3.so.${SHIBOKEN_VERSION} + ) + endif() + + if (NOT SHIBOKEN_INCLUDE_DIR) + return() + endif() + + find_file(SHIBOKEN_LIBRARY + ${SHIBOKEN_LIBRARY_BASENAMES} + PATHS ${SHIBOKEN_BASEDIR} + NO_DEFAULT_PATH) + + find_program(SHIBOKEN_BINARY + shiboken2 + PATHS ${SHIBOKEN_GENERATOR_BASEDIR} + NO_DEFAULT_PATH + ) + + message(STATUS "Shiboken include dir: ${SHIBOKEN_INCLUDE_DIR}") + message(STATUS "Shiboken library: ${SHIBOKEN_LIBRARY}") + message(STATUS "Shiboken binary: ${SHIBOKEN_BINARY}") + + if (SHIBOKEN_INCLUDE_DIR AND SHIBOKEN_LIBRARY AND SHIBOKEN_BINARY) + set(SHIBOKEN_FOUND TRUE) + endif() + +else() + set(SHIBOKEN_FOUND FALSE) +endif() + +if(MSVC) + # On Windows we must link to python3.dll that is a small library that links against python3x.dll + # that allow us to choose any python3x.dll at runtime + execute_process( + COMMAND ${Python3_EXECUTABLE} -c "if True: + for lib in '${Python3_LIBRARIES}'.split(';'): + if '/' in lib: + prefix, py = lib.rsplit('/', 1) + if py.startswith('python3'): + print(prefix + '/python3.lib') + break + " + OUTPUT_VARIABLE PYTHON_LIMITED_LIBRARIES + OUTPUT_STRIP_TRAILING_WHITESPACE + ) +else() + # On Linux and MacOs our modules should not link with any python library + # that must be handled by the main process + set(PYTHON_LIMITED_LIBRARIES "") +endif() + +if (SHIBOKEN_FOUND) + # Create shiboke2 target + add_library(Shiboken2::libshiboken SHARED IMPORTED GLOBAL) + if(MSVC) + set_property(TARGET Shiboken2::libshiboken PROPERTY + IMPORTED_IMPLIB ${SHIBOKEN_LIBRARY}) + endif() + set_property(TARGET Shiboken2::libshiboken PROPERTY + IMPORTED_LOCATION ${SHIBOKEN_LIBRARY}) + set_property(TARGET Shiboken2::libshiboken APPEND PROPERTY + INTERFACE_INCLUDE_DIRECTORIES ${SHIBOKEN_INCLUDE_DIR} ${Python3_INCLUDE_DIRS}) + set_property(TARGET Shiboken2::libshiboken APPEND PROPERTY + INTERFACE_LINK_LIBRARIES ${PYTHON_LIMITED_LIBRARIES}) +endif() + +find_package_handle_standard_args(Shiboken2 + REQUIRED_VARS SHIBOKEN_BASEDIR SHIBOKEN_INCLUDE_DIR SHIBOKEN_LIBRARY SHIBOKEN_BINARY + VERSION_VAR SHIBOKEN_VERSION +) diff --git a/cmake/Python/PySide2ModuleBuild.cmake b/cmake/Python/PySide2ModuleBuild.cmake new file mode 100644 index 00000000..2cfb6f6a --- /dev/null +++ b/cmake/Python/PySide2ModuleBuild.cmake @@ -0,0 +1,141 @@ +### +# This file is part of KDDockWidgets. +# +# Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +# Author: Renato Araujo Oliveira Filho +# +# This program is free software you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +## + +if (WIN32) + set(PATH_SEP "\;") +else() + set(PATH_SEP ":") +endif() +if (NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) +endif() +# Flags that we will pass to shiboken-generator +# --generator-set=shiboken: tells the generator that we want to use shiboken to generate code, +# a doc generator is also available +# --enable-parent-ctor-heuristic: Enable heuristics to detect parent relationship on constructors, +# this try to guess parent ownership based on the arguments of the constructors +# --enable-pyside-extensionsL: This will generate code for Qt based classes, adding extra attributes, +# like signal, slot; +# --enable-return-value-heuristic: Similar as --enable-parent-ctor-heuristic this use some logic to guess +# parent child relationship based on the returned argument +# --use-isnull-as-nb_nonzero: If a class have an isNull() const method, it will be used to compute +# the value of boolean casts. +# Example, QImage::isNull() will be used when on python side you do `if (myQImage)` +set(GENERATOR_EXTRA_FLAGS --generator-set=shiboken + --enable-parent-ctor-heuristic + --enable-pyside-extensions + --enable-return-value-heuristic + --use-isnull-as-nb_nonzero + -std=c++${CMAKE_CXX_STANDARD}) +macro(make_path varname) + # accepts any number of path variables + string(REPLACE ";" "${PATH_SEP}" ${varname} "${ARGN}") +endmacro() + +# Creates a PySide module target based on the arguments +# This will: +# 1 - Create a Cmake custom-target that call shiboken-generator passign the correct arguments +# 2 - Create a Cmake library target called "Py${LIBRARY_NAME}" the output name of this target +# will be changed to match PySide template +# Args: +# LIBRARY_NAME - The name of the output module +# TYPESYSTEM_PATHS - A list of paths where shiboken should look for typesystem files +# INCLUDE_PATHS - Include pahts necessary to parse your class. *This is not the same as build* +# OUTPUT_SOURCES - The files that will be generated by shiboken +# TARGET_INCLUDE_DIRS - This will be passed to target_include_directories +# TARGET_LINK_LIBRARIES - This will be passed to target_link_libraries +# GLOBAL_INCLUDE - A header-file that contains alls classes that will be generated +# TYPESYSTEM_XML - The target binding typesystem (that should be the full path) +# DEPENDS - This var will be passed to add_custom_command(DEPENDS) so a new generation will be +# trigger if one of these files changes +# MODULE_OUTPUT_DIR - Where the library file should be stored +macro(CREATE_PYTHON_BINDINGS + LIBRARY_NAME + TYPESYSTEM_PATHS + INCLUDE_PATHS + OUTPUT_SOURCES + TARGET_INCLUDE_DIRS + TARGET_LINK_LIBRARIES + GLOBAL_INCLUDE + TYPESYSTEM_XML + DEPENDS + MODULE_OUTPUT_DIR) + + # Transform the path separators into something shiboken understands. + make_path(shiboken_include_dirs ${INCLUDE_PATHS}) + make_path(shiboken_typesystem_dirs ${TYPESYSTEM_PATHS}) + get_property(raw_python_dir_include_dirs DIRECTORY PROPERTY INCLUDE_DIRECTORIES) + make_path(python_dir_include_dirs ${raw_python_dir_include_dirs}) + set(shiboken_include_dirs "${shiboken_include_dirs}${PATH_SEP}${python_dir_include_dirs}") + + set(shiboken_framework_include_dirs_option "") + if(CMAKE_HOST_APPLE) + set(shiboken_framework_include_dirs "${QT_FRAMEWORK_INCLUDE_DIR}") + make_path(shiboken_framework_include_dirs ${shiboken_framework_include_dirs}) + set(shiboken_framework_include_dirs_option "--framework-include-paths=${shiboken_framework_include_dirs}") + endif() + + set_property(SOURCE ${OUTPUT_SOURCES} PROPERTY SKIP_AUTOGEN ON) + add_custom_command(OUTPUT ${OUTPUT_SOURCES} + COMMAND ${SHIBOKEN_BINARY} ${GENERATOR_EXTRA_FLAGS} + ${GLOBAL_INCLUDE} + --include-paths=${shiboken_include_dirs} + --typesystem-paths=${shiboken_typesystem_dirs} + ${shiboken_framework_include_dirs_option} + --output-directory=${CMAKE_CURRENT_BINARY_DIR} + ${TYPESYSTEM_XML} + DEPENDS ${TYPESYSTEM_XML} ${DEPENDS} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Running generator for ${LIBRARY_NAME} binding...") + + set(TARGET_NAME "Py${LIBRARY_NAME}") + set(MODULE_NAME "${LIBRARY_NAME}") + add_library(${TARGET_NAME} MODULE ${OUTPUT_SOURCES}) + + set_target_properties(${TARGET_NAME} PROPERTIES + PREFIX "" + OUTPUT_NAME ${MODULE_NAME} + LIBRARY_OUTPUT_DIRECTORY ${MODULE_OUTPUT_DIR} + ) + + if(WIN32) + set_target_properties(${TARGET_NAME} PROPERTIES SUFFIX ".pyd") + endif() + + target_include_directories(${TARGET_NAME} PUBLIC + ${TARGET_INCLUDE_DIRS} + ${PYSIDE_EXTRA_INCLUDES} + ) + + target_link_libraries(${TARGET_NAME} + ${TARGET_LINK_LIBRARIES} + PySide2::pyside2 + Shiboken2::libshiboken + ) + target_compile_definitions(${TARGET_NAME} + PRIVATE Py_LIMITED_API=0x03050000 + ) + if(APPLE) + set_property(TARGET ${TARGET_NAME} APPEND PROPERTY + LINK_FLAGS "-undefined dynamic_lookup") + endif() + install(TARGETS ${TARGET_NAME} + LIBRARY DESTINATION ${Python3_SITELIB}/${TARGET_NAME}) +endmacro() diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt new file mode 100644 index 00000000..a6c3979b --- /dev/null +++ b/python/CMakeLists.txt @@ -0,0 +1,7 @@ +set(PYTHON_VERSION "3.7" CACHE STRING "Use specific python version to build the project.") +find_package(Python3 ${PYTHON_VERSION} REQUIRED COMPONENTS Interpreter Development) +find_package(Shiboken2 REQUIRED) +find_package(PySide2 ${Qt5Widgets_VERSION} EXACT REQUIRED) +include(PySide2ModuleBuild) + +add_subdirectory(PyKDDockWidgets) diff --git a/python/PyKDDockWidgets/CMakeLists.txt b/python/PyKDDockWidgets/CMakeLists.txt new file mode 100644 index 00000000..5d01cb03 --- /dev/null +++ b/python/PyKDDockWidgets/CMakeLists.txt @@ -0,0 +1,71 @@ + +# Auto-Genereate files every class will have his cpp/h files +set(PyKDDockWidgets_SRC + # individual classes + ${CMAKE_CURRENT_BINARY_DIR}/KDDockWidgets/kddockwidgets_dockwidgetbase_wrapper.cpp + ${CMAKE_CURRENT_BINARY_DIR}/KDDockWidgets/kddockwidgets_dockwidgetbase_wrapper.h + ${CMAKE_CURRENT_BINARY_DIR}/KDDockWidgets/kddockwidgets_dockwidget_wrapper.cpp + ${CMAKE_CURRENT_BINARY_DIR}/KDDockWidgets/kddockwidgets_dockwidget_wrapper.h + ${CMAKE_CURRENT_BINARY_DIR}/KDDockWidgets/kddockwidgets_mainwindowbase_wrapper.cpp + ${CMAKE_CURRENT_BINARY_DIR}/KDDockWidgets/kddockwidgets_mainwindowbase_wrapper.h + ${CMAKE_CURRENT_BINARY_DIR}/KDDockWidgets/kddockwidgets_mainwindow_wrapper.cpp + ${CMAKE_CURRENT_BINARY_DIR}/KDDockWidgets/kddockwidgets_mainwindow_wrapper.h + # namespace wrapper + ${CMAKE_CURRENT_BINARY_DIR}/KDDockWidgets/kddockwidgets_wrapper.cpp + ${CMAKE_CURRENT_BINARY_DIR}/KDDockWidgets/kddockwidgets_wrapper.h + # global module wrapper + ${CMAKE_CURRENT_BINARY_DIR}/KDDockWidgets/kddockwidgets_module_wrapper.cpp + ${CMAKE_CURRENT_BINARY_DIR}/KDDockWidgets/kddockwidgets_python.h +) + +# includes necessary to parse and build the classes specified on typesystem +set(PyKDDockWidgets_include_paths + $,${PATH_SEP}> +) + +# A list of paths where shiboken should look for typesystem +set(PyKDDockWidgets_typesystem_paths + # PySide path, this variable was exposed by FindPySide2.cmake + ${PYSIDE_TYPESYSTEMS} +) + +# Include flags/path that will be set in 'target_include_directories' +set(PyKDDockWidgets_target_include_directories + ${CMAKE_SOURCE_DIR} +) + +# Libraries that will be necessary to link the target, this will used in the command 'target_link_libraries' +set(PyKDDockWidgets_target_link_libraries + KDAB::kddockwidgets + Qt5::Core + Qt5::Gui + Qt5::Widgets +) + +# changes on these files should trigger a new generation +set(PyKDDockWidgets_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/kddockwidgets_global.h + ${CMAKE_SOURCE_DIR}/src/DockWidgetBase.h + ${CMAKE_SOURCE_DIR}/src/DockWidget.h + ${CMAKE_SOURCE_DIR}/src/MainWindowBase.h + ${CMAKE_SOURCE_DIR}/src/MainWindow.h +) + +CREATE_PYTHON_BINDINGS( + "KDDockWidgets" + "${PyKDDockWidgets_typesystem_paths}" + "${PyKDDockWidgets_include_paths}" + "${PyKDDockWidgets_SRC}" + "${PyKDDockWidgets_target_include_directories}" + "${PyKDDockWidgets_target_link_libraries}" + ${CMAKE_CURRENT_SOURCE_DIR}/kddockwidgets_global.h + ${CMAKE_CURRENT_SOURCE_DIR}/typesystem_kddockwidgets.xml + "${PyKDDockWidgets_DEPENDS}" + ${CMAKE_CURRENT_BINARY_DIR} +) + +# Make moduled import from build dir works +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/__init__.py ${CMAKE_CURRENT_BINARY_DIR}/__init__.py) + +# install +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/__init__.py DESTINATION ${Python3_SITELIB}/PyKDDockWidgets) diff --git a/python/PyKDDockWidgets/__init__.py b/python/PyKDDockWidgets/__init__.py new file mode 100644 index 00000000..cdd7a7c6 --- /dev/null +++ b/python/PyKDDockWidgets/__init__.py @@ -0,0 +1,14 @@ +__all__ = ['KDDockWidgets'] + +# Preload PySide2 libraries to avoid missing libraries while loading KDDockWidgets +try: + from PySide2 import QtCore +except Exception: + print("Failed to lod PySide") + raise + +# avoid duplicate namespace, due the PYSIDE-1325 bug I will have my package like this +# PyKDDockWidgets.KDDockWidgets.KDDockWidgets.MainWindow +# To avoid this I add a WORKAROUND to reduce it +from .KDDockWidgets import KDDockWidgets as _priv +KDDockWidgets = _priv diff --git a/python/PyKDDockWidgets/kddockwidgets_global.h b/python/PyKDDockWidgets/kddockwidgets_global.h new file mode 100644 index 00000000..cdb1af47 --- /dev/null +++ b/python/PyKDDockWidgets/kddockwidgets_global.h @@ -0,0 +1,13 @@ +#pragma once + +// Make "signals:", "slots:" visible as access specifiers +#define QT_ANNOTATE_ACCESS_SPECIFIER(a) __attribute__((annotate(#a))) + +// Define PYTHON_BINDINGS this will be used in some part of c++ to skip problematic parts +#define PYTHON_BINDINGS + +#include +#include +#include +#include + diff --git a/python/PyKDDockWidgets/typesystem_kddockwidgets.xml b/python/PyKDDockWidgets/typesystem_kddockwidgets.xml new file mode 100644 index 00000000..6d0f8dbb --- /dev/null +++ b/python/PyKDDockWidgets/typesystem_kddockwidgets.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/examples/MyMainWindow.py b/python/examples/MyMainWindow.py new file mode 100644 index 00000000..73cf92ca --- /dev/null +++ b/python/examples/MyMainWindow.py @@ -0,0 +1,170 @@ +################################################################################ +## This file is part of KDDockWidgets. +## +## Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +## Author: Renato Araujo Oliveira Filho +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program. If not, see . +################################################################################ + +from PySide2 import QtCore, QtWidgets, QtGui +from PyKDDockWidgets import KDDockWidgets + +from MyWidget1 import MyWidget1 +from MyWidget2 import MyWidget2 +from MyWidget3 import MyWidget3 + +def newMyWidget(parent = None): + randomNumber = QtCore.QRandomGenerator.global_().bounded(0, 100) + 1 + if (randomNumber < 50): + if (randomNumber < 33): + return MyWidget1(parent) + else: + return MyWidget3(parent) + else: + return MyWidget2(parent) + +class MyMainWindow(KDDockWidgets.MainWindow): + s_count = 0 + s_menuCount = 0 + + def __init__(self, uniqueName, options = KDDockWidgets.MainWindowOption_None, dockWidget0IsNonClosable = False, nonDockableDockWidget9 = False, restoreIsRelative = False, maxSizeForDockWidget8 = False, affinityName = "", parent = None): + super().__init__(uniqueName, options, parent) + self.m_dockWidget0IsNonClosable = dockWidget0IsNonClosable + self.m_dockWidget9IsNonDockable = nonDockableDockWidget9 + self.m_restoreIsRelative = restoreIsRelative + self.m_maxSizeForDockWidget8 = maxSizeForDockWidget8 + self.m_dockwidgets = [] + + menubar = self.menuBar() + fileMenu = QtWidgets.QMenu("File") + self.m_toggleMenu = QtWidgets.QMenu("Toggle") + menubar.addMenu(fileMenu) + menubar.addMenu(self.m_toggleMenu) + + newAction = fileMenu.addAction("New DockWidget") + newAction.triggered.connect(self._newDockWidget) + + saveLayoutAction = fileMenu.addAction("Save Layout") + saveLayoutAction.triggered.connect(self._saveLayout) + + restoreLayoutAction = fileMenu.addAction("Restore Layout") + restoreLayoutAction.triggered.connect(self._restoreLayout) + + closeAllAction = fileMenu.addAction("Close All") + closeAllAction.triggered.connect(self._closeAll) + + layoutEqually = fileMenu.addAction("Layout Equally") + layoutEqually.triggered.connect(self.layoutEqually) + + quitAction = fileMenu.addAction("Quit") + quitAction.triggered.connect(QtWidgets.QApplication.instance().quit) + + self.setAffinities([ affinityName ]) + self.createDockWidgets() + + def _newDockWidget(self): + MyMainWindow.s_menuCount += 1 + w = newMyWidget(self) + w.setGeometry(100, 100, 400, 400) + dock = KDDockWidgets.DockWidget("new dock %d"%(MyMainWindow.s_menuCount)) + dock.setWidget(w) + dock.resize(600, 600) + dock.show() + self.m_dockwidgets.append(dock) + + def _saveLayout(self): + #saver = KDDockWidgets.LayoutSaver() + #result = saver.saveToFile("mylayout.json") + #print("Saving layout to disk. Result=", result) + print("Not available") + + def _restoreLayout(self): + #options = KDDockWidgets.RestoreOption_None + #if self.m_restoreIsRelative: + # options |= KDDockWidgets.RestoreOption_RelativeToMainWindow + #saver = KDDockWidgets.LayoutSaver(options) + #saver.restoreFromFile("mylayout.json") + print("Not available") + + def _closeAll(self): + for dw in self.m_dockwidgets: + dw.close() + + def createDockWidgets(self): + if self.m_dockWidget9IsNonDockable: + numDockWidgets = 10 + else: + numDockWidgets = 9 + + # numDockWidgets = 2 + # Create 9 KDDockWidget::DockWidget and the respective widgets they're hosting (MyWidget instances) + for i in range(numDockWidgets): + self.m_dockwidgets.append(self.newDockWidget()) + + # MainWindow::addDockWidget() attaches a dock widget to the main window: + self.addDockWidget(self.m_dockwidgets[0], KDDockWidgets.Location_OnTop) + + # Here, for finer granularity we specify right of dockwidgets[0]: + self.addDockWidget(self.m_dockwidgets[1], KDDockWidgets.Location_OnRight, self.m_dockwidgets[0]) + + self.addDockWidget(self.m_dockwidgets[2], KDDockWidgets.Location_OnLeft) + self.addDockWidget(self.m_dockwidgets[3], KDDockWidgets.Location_OnBottom) + self.addDockWidget(self.m_dockwidgets[4], KDDockWidgets.Location_OnBottom) + + # Tab two dock widgets together + self.m_dockwidgets[3].addDockWidgetAsTab(self.m_dockwidgets[5]) + + # 6 is floating, as it wasn't added to the main window via MainWindow::addDockWidget(). + # and we tab 7 with it. + self.m_dockwidgets[6].addDockWidgetAsTab(self.m_dockwidgets[7]) + + # Floating windows also support nesting, here we add 8 to the bottom of the group + self.m_dockwidgets[6].addDockWidgetToContainingWindow(self.m_dockwidgets[8], KDDockWidgets.Location_OnBottom) + + floatingWindow = self.m_dockwidgets[6].window() + floatingWindow.move(100, 100) + + def newDockWidget(self): + # Passing options is optional, we just want to illustrate Option_NotClosable here + options = KDDockWidgets.DockWidget.Option_None + if (MyMainWindow.s_count == 0) and self.m_dockWidget0IsNonClosable: + options |= KDDockWidgets.DockWidget.Option_NotClosable + + if (MyMainWindow.s_count == 9) and self.m_dockWidget9IsNonDockable: + options |= KDDockWidgets.DockWidget.Option_NotDockable + + dock = KDDockWidgets.DockWidget("DockWidget #%d"%(MyMainWindow.s_count), options) + dock.setAffinities(self.affinities()); # optional, just to show the feature. Pass -mi to the example to see incompatible dock widgets + + if MyMainWindow.s_count == 1: + dock.setIcon(QtGui.QIcon.fromTheme("mail-message")) + + myWidget = newMyWidget(self) + if (MyMainWindow.s_count == 8) and self.m_maxSizeForDockWidget8: + # Set a maximum size on dock #8 + myWidget.setMaximumSize(200, 200) + + dock.setWidget(myWidget) + + if dock.options() & KDDockWidgets.DockWidget.Option_NotDockable: + dock.setTitle("DockWidget #%d (%s)" %(MyMainWindow.s_count, "non dockable")) + else: + dock.setTitle("DockWidget #%d"%(MyMainWindow.s_count)) + + dock.resize(600, 600) + self.m_toggleMenu.addAction(dock.toggleAction()) + MyMainWindow.s_count += 1 + return dock + diff --git a/python/examples/MyWidget.py b/python/examples/MyWidget.py new file mode 100644 index 00000000..d018c5b0 --- /dev/null +++ b/python/examples/MyWidget.py @@ -0,0 +1,58 @@ +################################################################################ +## This file is part of KDDockWidgets. +## +## Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +## Author: Renato Araujo Oliveira Filho +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program. If not, see . +################################################################################ + +from PySide2 import QtWidgets, QtGui, QtCore + +class MyWidget(QtWidgets.QWidget): + s_images = {} + def __init__(self, backgroundFile, logoFile, parent = None): + super().__init__(parent) + + self.m_background = self._lookupImage(backgroundFile) + self.m_logo = self._lookupImage(logoFile) + + def _lookupImage(self, imageName): + if imageName == "": + return None + + if imageName not in MyWidget.s_images: + MyWidget.s_images[imageName] = QtGui.QImage(imageName) + + return MyWidget.s_images[imageName] + + def drawLogo(self, p): + + if not self.m_logo: + return + + ratio = self.m_logo.height() / (self.m_logo.width() * 1.0) + maxWidth = int(0.80 * self.size().width()) + maxHeight = int(0.80 * self.size().height()) + proposedHeight = int(maxWidth * ratio) + if (proposedHeight <= maxHeight): + width = maxWidth + else: + width = int(maxHeight / ratio) + + height = int(width * ratio) + targetLogoRect = QtCore.QRect(0,0, width, height) + targetLogoRect.moveCenter(self.rect().center() + QtCore.QPoint(0, -int(self.size().height() * 0.00))) + p.drawImage(targetLogoRect, self.m_logo, self.m_logo.rect()); + diff --git a/python/examples/MyWidget1.py b/python/examples/MyWidget1.py new file mode 100644 index 00000000..629579fa --- /dev/null +++ b/python/examples/MyWidget1.py @@ -0,0 +1,36 @@ +################################################################################ +## This file is part of KDDockWidgets. +## +## Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +## Author: Renato Araujo Oliveira Filho +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program. If not, see . +################################################################################ + +from PySide2 import QtWidgets, QtGui + +from MyWidget import MyWidget + + +class MyWidget1(MyWidget): + def __init__(self, parent = None): + super().__init__(":/assets/triangles.png", ":/assets/KDAB_bubble_white.png", parent) + + + def paintEvent(self, ev): + p = QtGui.QPainter(self) + p.fillRect(self.rect(), QtGui.QColor(0xCC, 0xCC, 0xCC)) + p.drawImage(self.m_background.rect(), self.m_background, self.m_background.rect()) + self.drawLogo(p) + diff --git a/python/examples/MyWidget2.py b/python/examples/MyWidget2.py new file mode 100644 index 00000000..d973d65f --- /dev/null +++ b/python/examples/MyWidget2.py @@ -0,0 +1,34 @@ +################################################################################ +## This file is part of KDDockWidgets. +## +## Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +## Author: Renato Araujo Oliveira Filho +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program. If not, see . +################################################################################ + +from PySide2 import QtWidgets, QtGui, QtCore + +from MyWidget import MyWidget + +class MyWidget2(MyWidget): + + def __init__(self, parent = None): + super().__init__("", ":/assets/KDAB_bubble_blue.png", parent) + + def paintEvent(self, ev): + p = QtGui.QPainter(self) + p.fillRect(self.rect(), QtCore.Qt.white); + self.drawLogo(p) + diff --git a/python/examples/MyWidget3.py b/python/examples/MyWidget3.py new file mode 100644 index 00000000..9658a89a --- /dev/null +++ b/python/examples/MyWidget3.py @@ -0,0 +1,39 @@ +################################################################################ +## This file is part of KDDockWidgets. +## +## Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +## Author: Renato Araujo Oliveira Filho +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program. If not, see . +################################################################################ + +from PySide2 import QtWidgets, QtGui, QtCore + +from MyWidget import MyWidget + +class MyWidget3(MyWidget): + + def __init__(self, parent = None): + super().__init__(":/assets/base.png", ":/assets/KDAB_bubble_fulcolor.png", parent) + self.m_triangle = QtGui.QImage(":/assets/tri.png") + + def paintEvent(self, ev): + p = QtGui.QPainter(self) + p.fillRect(self.rect(), QtGui.QColor(0xD5, 0xD5, 0xD5)) + p.drawImage(self.m_background.rect(), self.m_background, self.m_background.rect()) + + targetRect = QtCore.QRect(QtCore.QPoint(self.width() - self.m_triangle.width(), self.height() - self.m_triangle.height()), self.m_triangle.size()) + + self.drawLogo(p) + diff --git a/python/examples/README.txt b/python/examples/README.txt new file mode 100644 index 00000000..b19456b3 --- /dev/null +++ b/python/examples/README.txt @@ -0,0 +1,8 @@ +Running python example +====================== + +Generate resource file with: +~# rcc -g python -o rc_assets.py ../../examples/dockwidgets/resources_example.qrc + +Run the app: +~# python3 main.py diff --git a/python/examples/main.py b/python/examples/main.py new file mode 100644 index 00000000..111ba69a --- /dev/null +++ b/python/examples/main.py @@ -0,0 +1,44 @@ +################################################################################ +## This file is part of KDDockWidgets. +## +## Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +## Author: Renato Araujo Oliveira Filho +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program. If not, see . +################################################################################ + + +from PySide2 import QtWidgets, QtCore +from PyKDDockWidgets import KDDockWidgets +from MyMainWindow import MyMainWindow + +import sys +import rc_assets + +if __name__ == "__main__": + QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) + QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps) + app = QtWidgets.QApplication(sys.argv) + + app.setOrganizationName("KDAB") + app.setApplicationName("Test app") + app.setStyle(QtWidgets.QStyleFactory.create("Fusion")) + + mainWindow = MyMainWindow("MyMainWindow", ) + mainWindow.setWindowTitle("Main Window 1") + mainWindow.resize(1200, 1200) + mainWindow.show() + + app.exec_() + diff --git a/src/DockWidgetBase.h b/src/DockWidgetBase.h index 2752f999..3382d3b8 100644 --- a/src/DockWidgetBase.h +++ b/src/DockWidgetBase.h @@ -64,7 +64,11 @@ class StateDragging; * * Do not use instantiate directly in user code. Use DockWidget instead. */ +#ifndef PYTHON_BINDINGS //Pyside bug: https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1327 class DOCKS_EXPORT DockWidgetBase : public QWidgetOrQuick +#else +class DOCKS_EXPORT DockWidgetBase : public QWidget +#endif { Q_OBJECT public: diff --git a/src/MainWindowBase.h b/src/MainWindowBase.h index 194079bf..a5ecc47e 100644 --- a/src/MainWindowBase.h +++ b/src/MainWindowBase.h @@ -51,7 +51,11 @@ class DropAreaWithCentralFrame; * * Do not use instantiate directly in user code. Use MainWindow instead. */ +#ifndef PYTHON_BINDINGS //Pyside bug: https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1327 class DOCKS_EXPORT MainWindowBase : public QMainWindowOrQuick +#else +class DOCKS_EXPORT MainWindowBase : public QMainWindow +#endif { Q_OBJECT public: