From: Stuart Prescott Date: Tue, 10 Feb 2026 06:10:18 +0000 (+1100) Subject: New upstream version 6.10.2 X-Git-Tag: archive/raspbian/6.10.3-2+rpi1^2~18^2~1 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=aac62d8b967bf2b12404e20bcb1371af59664c43;p=pyside6.git New upstream version 6.10.2 --- diff --git a/build_scripts/main.py b/build_scripts/main.py index 57e337ac..1819b7e4 100644 --- a/build_scripts/main.py +++ b/build_scripts/main.py @@ -720,12 +720,12 @@ class PysideBuild(_build, CommandMixin, BuildInfoCollectorMixin): cmake_cmd.append(f"-DCMAKE_CXX_COMPILER_LAUNCHER={compiler_launcher}") if OPTION["SANITIZE_ADDRESS"]: + cmake_cmd.append("-DSANITIZE_ADDRESS=ON") + if OPTION["SANITIZE_THREAD"]: # Some simple sanity checking. Only use at your own risk. - if (sys.platform.startswith('linux') - or sys.platform.startswith('darwin')): - cmake_cmd.append("-DSANITIZE_ADDRESS=ON") - else: - raise SetupError("Address sanitizer can only be used on Linux and macOS.") + if sys.platform == "win32" and not self.is_cross_compile: + self.warn("Thread sanitizer may not be supported yet.") + cmake_cmd.append("-DSANITIZE_THREAD=ON") if extension.lower() == PYSIDE: pyside_qt_conf_prefix = '' diff --git a/build_scripts/options.py b/build_scripts/options.py index 5963a398..ba4039e9 100644 --- a/build_scripts/options.py +++ b/build_scripts/options.py @@ -208,6 +208,7 @@ class CommandMixin: ('verbose-build', None, 'Verbose build'), ('quiet', None, 'Quiet build'), ('sanitize-address', None, 'Build with address sanitizer'), + ('sanitize-thread', None, 'Build with thread sanitizer'), ('shorter-paths', None, 'Use shorter paths'), ('doc-build-online', None, 'Build online documentation'), ('qtpaths=', None, 'Path to qtpaths'), @@ -290,6 +291,7 @@ class CommandMixin: self.log_level = "info" self.verbose_build = False self.sanitize_address = False + self.sanitize_thread = False self.snapshot_build = False self.shorter_paths = False self.doc_build_online = False @@ -444,6 +446,7 @@ class CommandMixin: log.setLevel(logging.DEBUG) OPTION['SANITIZE_ADDRESS'] = self.sanitize_address + OPTION['SANITIZE_THREAD'] = self.sanitize_thread OPTION['SHORTER_PATHS'] = self.shorter_paths OPTION['DOC_BUILD_ONLINE'] = self.doc_build_online if self.unity: diff --git a/build_scripts/platforms/unix.py b/build_scripts/platforms/unix.py index 04403ba0..59c63fda 100644 --- a/build_scripts/platforms/unix.py +++ b/build_scripts/platforms/unix.py @@ -8,7 +8,7 @@ from pathlib import Path from ..log import log from ..config import config from ..options import OPTION -from ..utils import copydir, copyfile, copy_qt_metatypes, makefile +from ..utils import (copydir, copyfile, copy_qt_metatypes, makefile, copy_cmake_config_dirs) from .. import PYSIDE, SHIBOKEN from .linux import prepare_standalone_package_linux from .macos import prepare_standalone_package_macos @@ -108,11 +108,11 @@ def prepare_packages_posix(pyside_build, _vars, cross_build=False): "{st_build_dir}/{st_package_name}/scripts/shiboken_tool.py", force=False, _vars=_vars) - if config.is_internal_shiboken_generator_build() or config.is_internal_pyside_build(): - # /include/* -> /{st_package_name}/include + if config.is_internal_shiboken_module_build() or config.is_internal_pyside_build(): + # /{cmake_package_name}/include/* -> /{st_package_name}/include copydir( - "{install_dir}/include/{cmake_package_name}", - "{st_build_dir}/{st_package_name}/include", + "{install_dir}/{cmake_package_name}/include", + destination_dir / "include", _vars=_vars) if config.is_internal_pyside_build(): @@ -256,6 +256,26 @@ def prepare_packages_posix(pyside_build, _vars, cross_build=False): # Copy over clang before rpath patching. pyside_build.prepare_standalone_clang(is_win=False) + # Copy CMake config files + if config.is_internal_shiboken_generator_build(): + # Copy Shiboken6Tools CMake package for generator + copy_cmake_config_dirs( + _vars["install_dir"], _vars["st_build_dir"], + _vars["st_package_name"], "Shiboken6Tools" + ) + elif config.is_internal_shiboken_module_build(): + # Copy Shiboken6 CMake package for module + copy_cmake_config_dirs( + _vars["install_dir"], _vars["st_build_dir"], + _vars["st_package_name"], "Shiboken6" + ) + elif config.is_internal_pyside_build(): + # Copy PySide6 CMake package + copy_cmake_config_dirs( + _vars["install_dir"], _vars["st_build_dir"], + _vars["st_package_name"], "PySide6" + ) + # Update rpath to $ORIGIN if (sys.platform.startswith('linux') or sys.platform.startswith('darwin')) and not is_android: pyside_build.update_rpath(executables) diff --git a/build_scripts/platforms/windows_desktop.py b/build_scripts/platforms/windows_desktop.py index 073f390a..e023ff00 100644 --- a/build_scripts/platforms/windows_desktop.py +++ b/build_scripts/platforms/windows_desktop.py @@ -12,9 +12,8 @@ from pathlib import Path from ..log import log from ..config import config from ..options import OPTION -from ..utils import (copydir, copyfile, copy_qt_metatypes, - download_and_extract_7z, filter_match, makefile, in_coin, - coin_job_id) +from ..utils import (copydir, copyfile, copy_qt_metatypes, download_and_extract_7z, + filter_match, makefile, in_coin, coin_job_id, copy_cmake_config_dirs) from .. import PYSIDE, SHIBOKEN, PYSIDE_WINDOWS_BIN_TOOLS, PYSIDE_MULTIMEDIA_LIBS @@ -95,10 +94,10 @@ def prepare_packages_win32(pyside_build, _vars): _filter=pdbs, recursive=False, _vars=_vars) - if config.is_internal_shiboken_generator_build() or config.is_internal_pyside_build(): - # /include/* -> /{st_package_name}/include + if config.is_internal_shiboken_module_build() or config.is_internal_pyside_build(): + # /{cmake_package_name}/include/* -> /{st_package_name}/include copydir( - "{install_dir}/include/{cmake_package_name}", + "{install_dir}/{cmake_package_name}/include", destination_dir / "include", _vars=_vars) @@ -202,6 +201,8 @@ def prepare_packages_win32(pyside_build, _vars): copy_qt_artifacts(pyside_build, destination_qt_dir, copy_pdbs, _vars) download_qt_dependency_dlls(_vars, destination_dir, msvc_redist) + copy_cmake_packages(_vars) + # MSVC redistributable file list. msvc_redist = [ @@ -440,3 +441,33 @@ def copy_qt_artifacts(pyside_build, destination_qt_dir, copy_pdbs, _vars): if copy_clang or platform.machine() == "ARM64": # Qt CI is using dynamic libclang with arm config. pyside_build.prepare_standalone_clang(is_win=True) + + +def copy_cmake_packages(_vars): + if config.is_internal_shiboken_generator_build(): + print("copy_cmake_config_dirs called for Shiboken6Tools with:", + "_vars['install_dir'] =", _vars["install_dir"], + "_vars['st_build_dir'] =", _vars["st_build_dir"], + "_vars['st_package_name'] =", _vars["st_package_name"]) + copy_cmake_config_dirs( + _vars["install_dir"], _vars["st_build_dir"], + _vars["st_package_name"], "Shiboken6Tools" + ) + elif config.is_internal_shiboken_module_build(): + print("copy_cmake_config_dirs called for Shiboken6 with:", + "_vars['install_dir'] =", _vars["install_dir"], + "_vars['st_build_dir'] =", _vars["st_build_dir"], + "_vars['st_package_name'] =", _vars["st_package_name"]) + copy_cmake_config_dirs( + _vars["install_dir"], _vars["st_build_dir"], + _vars["st_package_name"], "Shiboken6" + ) + elif config.is_internal_pyside_build(): + print("copy_cmake_config_dirs called for PySide6 with:", + "_vars['install_dir'] =", _vars["install_dir"], + "_vars['st_build_dir'] =", _vars["st_build_dir"], + "_vars['st_package_name'] =", _vars["st_package_name"]) + copy_cmake_config_dirs( + _vars["install_dir"], _vars["st_build_dir"], + _vars["st_package_name"], "PySide6" + ) diff --git a/build_scripts/utils.py b/build_scripts/utils.py index eb0c8b0b..3cb7ade0 100644 --- a/build_scripts/utils.py +++ b/build_scripts/utils.py @@ -1140,3 +1140,48 @@ def parse_modules(modules: str) -> str: module_sub_set += ';' module_sub_set += m return module_sub_set + + +def copy_cmake_config_dirs(install_dir, st_build_dir, st_package_name, cmake_package_name): + """ + Copy all CMake config directories from /lib/cmake whose names start with + (case-insensitive) into //lib/cmake. + """ + src_cmake_dir = Path(install_dir) / "lib" / "cmake" + wheel_cmake_dir = Path(install_dir) / "lib" / "wheels" / "cmake" + dst_cmake_dir = Path(st_build_dir) / st_package_name / "lib" / "cmake" + dst_cmake_dir.mkdir(parents=True, exist_ok=True) + + for src_path in src_cmake_dir.iterdir(): + if src_path.is_dir() and (src_path.name.lower() == cmake_package_name.lower()): + dst_path = dst_cmake_dir / src_path.name + if dst_path.exists(): + shutil.rmtree(dst_path) + dst_path.mkdir(parents=True) + + # check for wheel target files + wheel_path = wheel_cmake_dir / src_path.name + wheel_targets_exist = {} + if wheel_path.exists(): + for item in wheel_path.iterdir(): + if item.is_file() and re.search(r"Targets(-.*)?\.cmake$", item.name): + base_name = item.name.split('Targets')[0] + if base_name in ("PySide6", "Shiboken6", "Shiboken6Tools"): + wheel_targets_exist[base_name] = True + # Copy wheel target file + shutil.copy2(str(item), str(dst_path / item.name)) + + # Copy remaining files + for item in src_path.iterdir(): + if item.is_file(): + skip_file = False + if re.search(r"Targets(-.*)?\.cmake$", item.name): + base_name = item.name.split('Targets')[0] + is_pyside_shiboken = base_name in ("PySide6", "Shiboken6", "Shiboken6Tools") + if is_pyside_shiboken and base_name in wheel_targets_exist: + skip_file = True + + if not skip_file: + shutil.copy2(str(item), str(dst_path / item.name)) + elif item.is_dir(): + shutil.copytree(str(item), str(dst_path / item.name)) diff --git a/build_scripts/wheel_files.py b/build_scripts/wheel_files.py index e9b936f1..6aaa6656 100644 --- a/build_scripts/wheel_files.py +++ b/build_scripts/wheel_files.py @@ -91,7 +91,7 @@ class ModuleData: self.glue.append(f"qt{_lo}.cpp") self.doc_glue.append(f"qt{_lo}.rst") if not len(self.metatypes): - self.metatypes.append(f"qt6{_lo}_relwithdebinfo_metatypes.json") + self.metatypes.append(f"qt6{_lo}_metatypes.json") # The PySide6 directory that gets packaged by the build_scripts # 'prepare_packages()' has a certain structure that depends on @@ -106,9 +106,7 @@ class ModuleData: self.qtlib = [f"{i}.*dll".replace("lib", "") for i in self.qtlib] self.qml = [f"qml/{i}" for i in self.qml] self.translations = [f"translations/{i}" for i in self.translations] - self.metatypes = [ - f"metatypes/{i}".replace("_relwithdebinfo", "") for i in self.metatypes - ] + self.metatypes = [f"metatypes/{i}" for i in self.metatypes] self.plugins = [f"plugins/{i}" for i in self.plugins] else: if sys.platform == "darwin": @@ -178,7 +176,6 @@ def wheel_files_pyside_essentials() -> list[ModuleData]: module_QtSvg(), module_QtSvgWidgets(), module_QtUiTools(), - module_QtExampleIcons(), # Only for plugins module_QtWayland(), # there are no bindings for these modules, but their binaries are @@ -319,6 +316,10 @@ def module_QtCore() -> ModuleData: data.qtlib.append("libicuuc*") data.qtlib.append("libicuio*") + # add the include folders for libpyside binaries + # this is useful for downstream cmake projects like QtBridges + data.include.append("pyside6/*.h") + return data @@ -327,17 +328,19 @@ def module_QtGui() -> ModuleData: _typesystems = [ "gui_common.xml", "typesystem_gui_common.xml", + "typesystem_gui_nativeinterface.xml", "typesystem_gui_mac.xml", "typesystem_gui_win.xml", "typesystem_gui_x11.xml", - "typesystem_gui_rhi.xml" + "typesystem_gui_rhi.xml", + "typesystem_gui_wayland.xml" ] _metatypes = [ - "qt6eglfsdeviceintegrationprivate_relwithdebinfo_metatypes.json", - "qt6eglfskmssupportprivate_relwithdebinfo_metatypes.json", - "qt6kmssupportprivate_relwithdebinfo_metatypes.json", - "qt6xcbqpaprivate_relwithdebinfo_metatypes.json", + "qt6eglfsdeviceintegrationprivate_metatypes.json", + "qt6eglfskmssupportprivate_metatypes.json", + "qt6kmssupportprivate_metatypes.json", + "qt6xcbqpaprivate_metatypes.json", ] _qtlib = [ @@ -408,7 +411,7 @@ def module_QtDBus() -> ModuleData: def module_QtDesigner() -> ModuleData: data = ModuleData("Designer") data.qtlib.append("libQt6DesignerComponents") - data.metatypes.append("qt6designercomponentsprivate_relwithdebinfo_metatypes.json") + data.metatypes.append("qt6designercomponentsprivate_metatypes.json") json_data = get_module_json_data("Designer") data.plugins = get_module_plugins(json_data) data.extra_files.append("Qt/plugins/assetimporters/libuip*") @@ -483,25 +486,25 @@ def module_QtQml() -> ModuleData: ] _metatypes = [ - "qt6labsanimation_relwithdebinfo_metatypes.json", - "qt6labsfolderlistmodel_relwithdebinfo_metatypes.json", - "qt6labsqmlmodels_relwithdebinfo_metatypes.json", - "qt6labssettings_relwithdebinfo_metatypes.json", - "qt6labssharedimage_relwithdebinfo_metatypes.json", - "qt6labswavefrontmesh_relwithdebinfo_metatypes.json", - "qt6packetprotocolprivate_relwithdebinfo_metatypes.json", - "qt6qmlcompilerprivate_relwithdebinfo_metatypes.json", - "qt6qmlcompilerplusprivate_relwithdebinfo_metatypes.json", - "qt6qmlcore_relwithdebinfo_metatypes.json", - "qt6qmldebugprivate_relwithdebinfo_metatypes.json", - "qt6qmldomprivate_relwithdebinfo_metatypes.json", - "qt6qmllintprivate_relwithdebinfo_metatypes.json", - "qt6qmllocalstorage_relwithdebinfo_metatypes.json", - "qt6qmlmodels_relwithdebinfo_metatypes.json", - "qt6qmlworkerscript_relwithdebinfo_metatypes.json", - "qt6qmlxmllistmodel_relwithdebinfo_metatypes.json", - "qt6qmlmeta_relwithdebinfo_metatypes.json", - "qt6labsplatform_relwithdebinfo_metatypes.json", + "qt6labsanimation_metatypes.json", + "qt6labsfolderlistmodel_metatypes.json", + "qt6labsqmlmodels_metatypes.json", + "qt6labssettings_metatypes.json", + "qt6labssharedimage_metatypes.json", + "qt6labswavefrontmesh_metatypes.json", + "qt6packetprotocolprivate_metatypes.json", + "qt6qmlcompilerprivate_metatypes.json", + "qt6qmlcompilerplusprivate_metatypes.json", + "qt6qmlcore_metatypes.json", + "qt6qmldebugprivate_metatypes.json", + "qt6qmldomprivate_metatypes.json", + "qt6qmllintprivate_metatypes.json", + "qt6qmllocalstorage_metatypes.json", + "qt6qmlmodels_metatypes.json", + "qt6qmlworkerscript_metatypes.json", + "qt6qmlxmllistmodel_metatypes.json", + "qt6qmlmeta_metatypes.json", + "qt6labsplatform_metatypes.json", ] _qml = [ @@ -553,22 +556,22 @@ def module_QtQml() -> ModuleData: def module_QtQuick() -> ModuleData: data = ModuleData("Quick") _metatypes = [ - "qt6quickcontrolstestutilsprivate_relwithdebinfo_metatypes.json", - "qt6quickdialogs2_relwithdebinfo_metatypes.json", - "qt6quickdialogs2quickimpl_relwithdebinfo_metatypes.json", - "qt6quickdialogs2utils_relwithdebinfo_metatypes.json", - "qt6quickeffectsprivate_relwithdebinfo_metatypes.json", - "qt6quicketest_relwithdebinfo_metatypes.json", - "qt6quicketestutilsprivate_relwithdebinfo_metatypes.json", - "qt6quicklayouts_relwithdebinfo_metatypes.json", - "qt6quickparticlesprivate_relwithdebinfo_metatypes.json", - "qt6quickshapesprivate_relwithdebinfo_metatypes.json", - "qt6quicktemplates2_relwithdebinfo_metatypes.json", - "qt6quicktest_relwithdebinfo_metatypes.json", - "qt6quicktestutilsprivate_relwithdebinfo_metatypes.json", - "qt6quicktimeline_relwithdebinfo_metatypes.json", - "qt6quickvectorimage_relwithdebinfo_metatypes.json", - "qt6quickvectorimagegeneratorprivate_relwithdebinfo_metatypes.json", + "qt6quickcontrolstestutilsprivate_metatypes.json", + "qt6quickdialogs2_metatypes.json", + "qt6quickdialogs2quickimpl_metatypes.json", + "qt6quickdialogs2utils_metatypes.json", + "qt6quickeffectsprivate_metatypes.json", + "qt6quicketest_metatypes.json", + "qt6quicketestutilsprivate_metatypes.json", + "qt6quicklayouts_metatypes.json", + "qt6quickparticlesprivate_metatypes.json", + "qt6quickshapesprivate_metatypes.json", + "qt6quicktemplates2_metatypes.json", + "qt6quicktest_metatypes.json", + "qt6quicktestutilsprivate_metatypes.json", + "qt6quicktimeline_metatypes.json", + "qt6quickvectorimage_metatypes.json", + "qt6quickvectorimagegeneratorprivate_metatypes.json", ] _qtlib = [ "libQt6QuickEffects", @@ -583,7 +586,8 @@ def module_QtQuick() -> ModuleData: "libQt6QuickTimeline", "libQt6QuickTimelineBlendTrees", "libQt6QuickVectorImage", - "libQt6QuickVectorImageGenerator" + "libQt6QuickVectorImageGenerator", + "libQt6QuickVectorImageHelpers" ] data.qtlib.extend(_qtlib) @@ -618,7 +622,7 @@ def module_QtQuickControls2() -> ModuleData: data.qtlib.append("libQt6QuickControls2IOSStyleImpl") data.qtlib.append("libQt6QuickControls2MacOSStyleImpl") - data.metatypes.append("qt6quickcontrols2impl_relwithdebinfo_metatypes.json") + data.metatypes.append("qt6quickcontrols2impl_metatypes.json") return data @@ -690,9 +694,9 @@ def module_QtWayland() -> ModuleData: ] _metatypes = [ - "qt6waylandclient_relwithdebinfo_metatypes.json", - "qt6waylandeglclienthwintegrationprivate_relwithdebinfo_metatypes.json", - "qt6wlshellintegrationprivate_relwithdebinfo_metatypes.json", + "qt6waylandclient_metatypes.json", + "qt6waylandeglclienthwintegrationprivate_metatypes.json", + "qt6wlshellintegrationprivate_metatypes.json", ] data.qtlib.extend(_qtlib) @@ -777,24 +781,24 @@ def module_QtQuick3D() -> ModuleData: ] _metatypes = [ - "qt63dquick_relwithdebinfo_metatypes.json", - "qt63dquickanimation_relwithdebinfo_metatypes.json", - "qt63dquickextras_relwithdebinfo_metatypes.json", - "qt63dquickinput_relwithdebinfo_metatypes.json", - "qt63dquickrender_relwithdebinfo_metatypes.json", - "qt63dquickscene2d_relwithdebinfo_metatypes.json", - "qt6quick3dassetimport_relwithdebinfo_metatypes.json", - "qt6quick3dassetutils_relwithdebinfo_metatypes.json", - "qt6quick3deffects_relwithdebinfo_metatypes.json", - "qt6quick3dglslparserprivate_relwithdebinfo_metatypes.json", - "qt6quick3dhelpers_relwithdebinfo_metatypes.json", - "qt6quick3diblbaker_relwithdebinfo_metatypes.json", - "qt6quick3dparticleeffects_relwithdebinfo_metatypes.json", - "qt6quick3dparticles_relwithdebinfo_metatypes.json", - "qt6quick3druntimerender_relwithdebinfo_metatypes.json", - "qt6quick3dutils_relwithdebinfo_metatypes.json", - "qt6shadertools_relwithdebinfo_metatypes.json", - "qt6quick3dxr_relwithdebinfo_metatypes.json" + "qt63dquick_metatypes.json", + "qt63dquickanimation_metatypes.json", + "qt63dquickextras_metatypes.json", + "qt63dquickinput_metatypes.json", + "qt63dquickrender_metatypes.json", + "qt63dquickscene2d_metatypes.json", + "qt6quick3dassetimport_metatypes.json", + "qt6quick3dassetutils_metatypes.json", + "qt6quick3deffects_metatypes.json", + "qt6quick3dglslparserprivate_metatypes.json", + "qt6quick3dhelpers_metatypes.json", + "qt6quick3diblbaker_metatypes.json", + "qt6quick3dparticleeffects_metatypes.json", + "qt6quick3dparticles_metatypes.json", + "qt6quick3druntimerender_metatypes.json", + "qt6quick3dutils_metatypes.json", + "qt6shadertools_metatypes.json", + "qt6quick3dxr_metatypes.json" ] json_data = get_module_json_data("Quick3DAssetImport") @@ -844,7 +848,7 @@ def module_QtWebEngineWidgets() -> ModuleData: def module_QtWebEngineQuick() -> ModuleData: data = ModuleData("WebEngineQuick") data.qtlib.append("libQt6WebEngineQuickDelegatesQml") - data.metatypes.append("qt6webenginequickdelegatesqml_relwithdebinfo_metatypes.json") + data.metatypes.append("qt6webenginequickdelegatesqml_metatypes.json") return data @@ -852,7 +856,7 @@ def module_QtWebEngineQuick() -> ModuleData: def module_QtCharts() -> ModuleData: data = ModuleData("Charts") data.qtlib.append("libQt6ChartsQml") - data.metatypes.append("qt6chartsqml_relwithdebinfo_metatypes.json") + data.metatypes.append("qt6chartsqml_metatypes.json") return data @@ -860,7 +864,7 @@ def module_QtCharts() -> ModuleData: def module_QtDataVisualization() -> ModuleData: data = ModuleData("DataVisualization") data.qtlib.append("libQt6DataVisualizationQml") - data.metatypes.append("qt6datavisualizationqml_relwithdebinfo_metatypes.json") + data.metatypes.append("qt6datavisualizationqml_metatypes.json") data.typesystems.append("datavisualization_common.xml") return data @@ -881,7 +885,7 @@ def module_QtGraphsWidgets() -> ModuleData: def module_QtMultimedia() -> ModuleData: data = ModuleData("Multimedia") data.qtlib.append("libQt6MultimediaQuick") - data.metatypes.append("qt6multimediaquickprivate_relwithdebinfo_metatypes.json") + data.metatypes.append("qt6multimediaquickprivate_metatypes.json") json_data = get_module_json_data("Multimedia") data.translations.append("qtmultimedia_*") @@ -914,7 +918,7 @@ def module_QtNetworkAuth() -> ModuleData: def module_QtPositioning() -> ModuleData: data = ModuleData("Positioning") data.qtlib.append("libQt6PositioningQuick") - data.metatypes.append("qt6positioningquick_relwithdebinfo_metatypes.json") + data.metatypes.append("qt6positioningquick_metatypes.json") json_data = get_module_json_data("Positioning") data.plugins = get_module_plugins(json_data) @@ -924,7 +928,7 @@ def module_QtPositioning() -> ModuleData: def module_QtRemoteObjects() -> ModuleData: data = ModuleData("RemoteObjects") data.qtlib.append("libQt6RemoteObjectsQml") - data.metatypes.append("qt6remoteobjectsqml_relwithdebinfo_metatypes.json") + data.metatypes.append("qt6remoteobjectsqml_metatypes.json") return data @@ -932,7 +936,7 @@ def module_QtRemoteObjects() -> ModuleData: def module_QtSensors() -> ModuleData: data = ModuleData("Sensors") data.qtlib.append("libQt6SensorsQuick") - data.metatypes.append("qt6sensorsquick_relwithdebinfo_metatypes.json") + data.metatypes.append("qt6sensorsquick_metatypes.json") json_data = get_module_json_data("Sensors") data.plugins = get_module_plugins(json_data) @@ -948,7 +952,7 @@ def module_QtSerialPort() -> ModuleData: def module_QtSpatialAudio() -> ModuleData: data = ModuleData("SpatialAudio") - data.metatypes.append("qt6spatialaudio_debug_metatypes.json") + data.metatypes.append("qt6spatialaudio_metatypes.json") return data @@ -956,7 +960,7 @@ def module_QtSpatialAudio() -> ModuleData: def module_QtStateMachine() -> ModuleData: data = ModuleData("StateMachine") data.qtlib.append("libQt6StateMachineQml") - data.metatypes.append("qt6statemachineqml_relwithdebinfo_metatypes.json") + data.metatypes.append("qt6statemachineqml_metatypes.json") return data @@ -964,7 +968,7 @@ def module_QtStateMachine() -> ModuleData: def module_QtScxml() -> ModuleData: data = ModuleData("Scxml") data.qtlib.append("libQt6ScxmlQml") - data.metatypes.append("qt6scxmlqml_relwithdebinfo_metatypes.json") + data.metatypes.append("qt6scxmlqml_metatypes.json") json_data = get_module_json_data("Scxml") data.plugins = get_module_plugins(json_data) @@ -1052,13 +1056,13 @@ def module_QtHttpServer() -> ModuleData: def module_QtLanguageServer() -> ModuleData: data = ModuleData("LanguageServer") - data.metatypes.append("qt6languageserverprivate_relwithdebinfo_metatypes.json") + data.metatypes.append("qt6languageserverprivate_metatypes.json") return data def module_QtJsonRpc() -> ModuleData: data = ModuleData("JsonRpc") - data.metatypes.append("qt6jsonrpcprivate_relwithdebinfo_metatypes.json") + data.metatypes.append("qt6jsonrpcprivate_metatypes.json") return data @@ -1076,11 +1080,6 @@ def module_QtAsyncio() -> ModuleData: return data -def module_QtExampleIcons() -> ModuleData: - data = ModuleData("ExampleIcons") - return data - - def module_QtWebView() -> ModuleData: data = ModuleData("WebView") json_data = get_module_json_data("WebView") diff --git a/coin/dependencies.yaml b/coin/dependencies.yaml index ad4751d2..9d0157f7 100644 --- a/coin/dependencies.yaml +++ b/coin/dependencies.yaml @@ -1,6 +1,6 @@ product_dependency: ../../qt/qt5: - ref: "af7939f2df81369a4a6501c9b41e6e68278269eb" + ref: "abfb3788bcfd08d6192899eb39a8ff34d7546da8" dependency_source: supermodule dependencies: [ "../../qt/qt3d", diff --git a/coin/instructions/common_environment.yaml b/coin/instructions/common_environment.yaml index 949eecad..478a0765 100644 --- a/coin/instructions/common_environment.yaml +++ b/coin/instructions/common_environment.yaml @@ -3,6 +3,12 @@ instructions: - type: EnvironmentVariable variableName: QTEST_ENVIRONMENT variableValue: "ci" + - type: EnvironmentVariable + variableName: PYTHON_BUILD_MIRROR_URL + variableValue: "https://ci-files01-hki.ci.qt.io/input/python/" + - type: EnvironmentVariable + variableName: PYTHON_BUILD_MIRROR_URL_SKIP_CHECKSUM + variableValue: "1" - type: EnvironmentVariable variableName: PYSIDE_VIRTUALENV variableValue: "{{.AgentWorkingDir}}\\pyside\\pyside-setup\\env" @@ -328,13 +334,84 @@ instructions: condition: property property: host.osVersion not_in_values: [RHEL_8_6, RHEL_8_8, RHEL_8_10] - # ToDo: can be removed after 3.11 is available on qt5#3.8 on macOS - # start of ToDo + - type: ExecuteCommand - command: "tools/install-p311.sh" + command: "pyenv install 3.13.7" maxTimeInSeconds: 14400 maxTimeBetweenOutput: 1200 ignoreExitCode: true + enable_if: + condition: and + conditions: + - condition: property + property: host.osVersion + equals_value: MacOS_15 + - condition: property + property: host.arch + equals_value: ARM64 + userMessageOnFailure: > + Failed to install python 3.13 + - type: PrependToEnvironmentVariable + variableName: PATH + variableValue: "/Users/qt/.pyenv/versions/3.13.7/bin:" + enable_if: + condition: and + conditions: + - condition: property + property: host.osVersion + equals_value: MacOS_15 + - condition: property + property: host.arch + equals_value: ARM64 + - type: EnvironmentVariable + variableName: interpreter + variableValue: "python3.13" + enable_if: + condition: and + conditions: + - condition: property + property: host.osVersion + equals_value: MacOS_15 + - condition: property + property: host.arch + equals_value: ARM64 + + - type: ExecuteCommand + command: "sudo subscription-manager refresh" + ignoreExitCode: true + maxTimeInSeconds: 6000 + maxTimeBetweenOutput: 1200 + enable_if: + condition: property + property: host.osVersion + in_values: [RHEL_9_4, RHEL_9_6] + userMessageOnFailure: > + Failed to refresh subscription. + + - type: ExecuteCommand + command: "sudo yum -y install python3.11-devel python3.11-pip" + maxTimeInSeconds: 14400 + maxTimeBetweenOutput: 1200 + enable_if: + condition: property + property: host.osVersion + in_values: [RHEL_9_4, RHEL_9_6] + userMessageOnFailure: > + Failed to install python 3.11 + + - type: EnvironmentVariable + variableName: interpreter + variableValue: "python3.11" + enable_if: + condition: property + property: host.osVersion + in_values: [RHEL_9_4, RHEL_9_6] + + # Use pyenv to install Python 3.11 on macOS+Android + - type: ExecuteCommand + command: "pyenv install 3.11.9" + maxTimeInSeconds: 14400 + maxTimeBetweenOutput: 1200 enable_if: condition: and conditions: @@ -345,10 +422,10 @@ instructions: property: target.os equals_value: Android userMessageOnFailure: > - Failed to install python 3.11 + Failed to install python 3.11 with pyenv - type: PrependToEnvironmentVariable variableName: PATH - variableValue: "/Users/qt/python311/bin:" + variableValue: "/Users/qt/.pyenv/versions/3.11.9/bin:" enable_if: condition: and conditions: @@ -358,7 +435,6 @@ instructions: - condition: property property: target.os equals_value: Android - # end of ToDo - type: ExecuteCommand command: "virtualenv -p {{.Env.interpreter}} {{.AgentWorkingDir}}/env" maxTimeInSeconds: 14400 diff --git a/coin/instructions/execute_android_instructions.yaml b/coin/instructions/execute_android_instructions.yaml index ab4d0299..0c031bf4 100644 --- a/coin/instructions/execute_android_instructions.yaml +++ b/coin/instructions/execute_android_instructions.yaml @@ -32,7 +32,7 @@ instructions: userMessageOnFailure: > Failed to install deps - type: ExecuteCommand - command: "{{.Env.interpreter}} tools/cross_compile_android/main.py --qt-install-path /Users/qt/work/install --auto-accept-license --skip-update --verbose --ndk-path {{.Env.ANDROID_NDK_ROOT_DEFAULT}} --sdk-path {{.Env.ANDROID_SDK_ROOT}} --plat-name aarch64 --coin" + command: "{{.Env.interpreter}} tools/cross_compile_android/main.py --qt-install-path /Users/qt/work/install --auto-accept-license --skip-update --verbose --ndk-path {{.Env.ANDROID_NDK_ROOT}} --sdk-path {{.Env.ANDROID_SDK_ROOT}} --plat-name aarch64 --coin" maxTimeInSeconds: 14400 maxTimeBetweenOutput: 1200 ignoreExitCode: true @@ -43,7 +43,7 @@ instructions: userMessageOnFailure: > Failed to execute build instructions on macOS - type: ExecuteCommand - command: "{{.Env.interpreter}} tools/cross_compile_android/main.py --qt-install-path /home/qt/work/install --auto-accept-license --skip-update --verbose --ndk-path {{.Env.ANDROID_NDK_ROOT_DEFAULT}} --sdk-path {{.Env.ANDROID_SDK_ROOT}} --plat-name x86_64 --coin" + command: "{{.Env.interpreter}} tools/cross_compile_android/main.py --qt-install-path /home/qt/work/install --auto-accept-license --skip-update --verbose --ndk-path {{.Env.ANDROID_NDK_ROOT}} --sdk-path {{.Env.ANDROID_SDK_ROOT}} --plat-name x86_64 --coin" maxTimeInSeconds: 14400 maxTimeBetweenOutput: 1200 ignoreExitCode: true diff --git a/coin/instructions_utils.py b/coin/instructions_utils.py index 52c8211e..0337a0a4 100644 --- a/coin/instructions_utils.py +++ b/coin/instructions_utils.py @@ -148,8 +148,12 @@ def setup_virtualenv(python, exe, env, pip, log, ci): # Within Ubuntu 24.04 one can't install anything with pip to outside of # virtual env. Trust that we already have proper virtualenv installed. if os.environ.get("HOST_OSVERSION_COIN") != "ubuntu_24_04": + virtualenv_version = "20.7.2" + # 20.7.2 is too old for 3.13 + if sys.version_info[1] > 12: + virtualenv_version = "20.32.0" run_instruction( - [str(python), "-m", "pip", "install", "--user", "virtualenv==20.7.2"], + [str(python), "-m", "pip", "install", "--user", "virtualenv==" + virtualenv_version], "Failed to pin virtualenv", ) # installing to user base might not be in PATH by default. diff --git a/coin/module_config.yaml b/coin/module_config.yaml index 471c8403..a9f83666 100644 --- a/coin/module_config.yaml +++ b/coin/module_config.yaml @@ -21,7 +21,7 @@ accept_configuration: not_in_values: [Mingw, MSVC2015,Clang] - condition: property # Webassembly property: target.osVersion - not_equals_value: WebAssembly + not_in_values: [WebAssembly, RHEL_8_10] - condition: property # Windows on Arm property: target.arch not_equals_value: ARM64 @@ -31,6 +31,9 @@ accept_configuration: - condition: property property: target.osVersion not_equals_value: VxWorks + - condition: property + property: host.osVersion + not_equals_value: Windows_11_22H2 - condition: and conditions: - condition: property @@ -73,7 +76,31 @@ accept_configuration: - condition: property property: features contains_value: Packaging - + - condition: and + conditions: + - condition: property + property: target.os + equals_value: MacOS + - condition: property + property: features + contains_value: Packaging + - condition: property + property: target.arch + equals_value: X86_64-ARM64 + - condition: and + conditions: + - condition: property + property: target.os + equals_value: Windows + - condition: property + property: features + contains_value: Packaging + - condition: property + property: target.arch + equals_value: X86_64 + - condition: property + property: target.compiler + equals_value: Mingw machine_type: Build: cores: 8 diff --git a/create_wheels.py b/create_wheels.py index 55d02928..6f8d8812 100644 --- a/create_wheels.py +++ b/create_wheels.py @@ -60,13 +60,13 @@ def create_module_plugin_json(wheel_name: str, data: list[ModuleData], package_p json.dump(all_plugins, fp, indent=4) -def get_manifest(wheel_name: str, data: list[ModuleData], package_path: Path) -> str: +def get_manifest(wheel_name: str, data: list[ModuleData], package_path: Path, verbose: int) -> str: lines = [] for module in data: # It's crucial to have this adjust method here # because it include all the necessary modifications to make - # our soltuion work on the three main platforms. + # our solution work on the three main platforms. module.adjusts_paths_and_extensions() for field in module.get_fields(): @@ -76,12 +76,15 @@ def get_manifest(wheel_name: str, data: list[ModuleData], package_path: Path) -> if field == "ext": continue for line in getattr(module, field): + file = f"PySide6/{line}" + if verbose > 0 and "*" not in file and not Path(package_path / file).exists(): + print(f"Warning: {file} does not exist.", file=sys.stderr) if field in ("extra_dirs", "qml", "plugins"): - lines.append(f"graft PySide6/{line}") + lines.append(f"graft {file}") elif field == "qtlib" and sys.platform == "darwin": - lines.append(f"graft PySide6/{line}") + lines.append(f"graft {file}") else: - lines.append(f"include PySide6/{line}") + lines.append(f"include {file}") lines.append("recursive-exclude PySide6 *qt.conf*") lines.append("") @@ -92,6 +95,10 @@ def get_manifest(wheel_name: str, data: list[ModuleData], package_path: Path) -> # adding PySide6_Essentials.json and PySide6_Addons.json lines.append(f"include PySide6/{wheel_name}.json") + # Only include CMake configs for PySide6_Essentials + if wheel_name == "PySide6_Essentials": + lines.append("graft PySide6/lib/cmake") + return "\n".join(lines) @@ -368,7 +375,7 @@ def check_modules_consistency(): if len(missing_modules): print("Warning: the following modules don't have a function " - f"in 'build_scripts/wheel_files.py':\n {missing_modules}") + f"in 'build_scripts/wheel_files.py':\n {missing_modules}", file=sys.stderr) # Check READMEs readme_modules = set() @@ -382,12 +389,13 @@ def check_modules_consistency(): if len(missing_modules_readme): print("Warning: the following modules are not in READMEs :" - f"\n {missing_modules_readme}") + f"\n {missing_modules_readme}", file=sys.stderr) if __name__ == "__main__": parser = ArgumentParser() + parser.add_argument('--verbose', '-v', type=int, help='Verbose level') # Command line option to find the build/a/package_for_wheels parser.add_argument( "--env", type=str, default=None, @@ -411,7 +419,7 @@ if __name__ == "__main__": build_directory = get_build_directory(options) - verbose = False + verbose = options.verbose if options.verbose else 0 # Setup paths current_path = Path(__file__).resolve().parent artifacts_path = Path("wheel_artifacts/") @@ -480,7 +488,7 @@ if __name__ == "__main__": if data is None: manifest_content = get_simple_manifest(name) else: - manifest_content = get_manifest(name, data, package_path) + manifest_content = get_manifest(name, data, package_path, verbose) with open(package_path / "MANIFEST.in", "w") as f: f.write(manifest_content) @@ -494,7 +502,7 @@ if __name__ == "__main__": # 6. call the build module to create the wheel print("-- Creating wheels") - if not verbose: + if verbose < 2: _runner = pyproject_hooks.quiet_subprocess_runner else: _runner = pyproject_hooks.default_subprocess_runner diff --git a/doc/changelogs/changes-6.10.0 b/doc/changelogs/changes-6.10.0 new file mode 100644 index 00000000..630df3ff --- /dev/null +++ b/doc/changelogs/changes-6.10.0 @@ -0,0 +1,88 @@ +Qt for Python 6.10.0 is a minor release. + +For more details, refer to the online documentation included in this +distribution. The documentation is also available online: + +https://doc.qt.io/qtforpython/ + +Some of the changes listed in this file include issue tracking numbers +corresponding to tasks in the Qt Bug Tracker: + +https://bugreports.qt.io/ + +Each of these identifiers can be entered in the bug tracker to obtain more +information about a particular change. + +* ************************************************************************** +* PySide6 * +**************************************************************************** + +PySide now uses multi-phase Python extension module initialization for the +non-deployed case. For scripting appplications, this implies that it is no +longer sufficient to call the Init() function of a module to load the module +in order to be able to access its type structs for binding variables. +Instead, PyImport_ImportModule() must be used (see scriptable application +example). + + - [PYSIDE-1735] @QEnum, @QFlag and QMetaEnum now support unsigned 64 bit + values for QML usage (with the exception of Qt Widgets + Designer), following the support added to Qt. + - [PYSIDE-2840] It is now possible to use @QEnum/@QFlag-decorated + enumerations as properties of custom widgets in Qt Widgets + Designer. + - [PYSIDE-2916] The invocation of functions overridden in Python + has been optimized for speed. + - [PYSIDE-3012] type hints: The annotations of QPropertyAnimation + have been fixed. + - [PYSIDE-3084] Enumerations are now stored as such instead of an opaque + PyObjectWrapper in functions returning QVariant. + - [PYSIDE-3137] A warning occurring when doing the first signal connection + from a thread has been fixed. + - [PYSIDE-3143] A bug causing events to be wrongly converted to + QStandardItem has been fixed. Also, a leak of QStandardItem + instances returned from QStandardItemModel functions + has been fixed. + - [PYSIDE-3146] Deployment: Values generated into pysidedeploy.spec are + now sorted. + - [PYSIDE-3147] Initial adaptations for the upcoming Python version 3.14 + have been done. + - [PYSIDE-3164] type hints: Enum values have been added to the stubs for + improved type checking. + - [PYSIDE-3178] type hints: The return types of + QPoint(F)/QSize(F).toTuple have been fixed. + - [QTBUG-110428] The QtExampleIcons module has been removed due to + the removal of the underlying library in Qt. + + *************************************************************************** +* Shiboken6 * +**************************************************************************** + + - Template specializations like "std::optional" can now be specified + as primitive types with converter code. + - [PYSIDE-2221] Multi-phase Python extension module initialization is now + used for the non-deployed case. + - [PYSIDE-3011] It is now possible to inject code into the the wrapper + class declaration, which can be used to import base class + members via the "using" keyword. + - [PYSIDE-3105] The support for cross compiling (using the correct target + for clang-based parsing) has been improved. Various options + have been added to shiboken to be able to specify target + platform and compiler. + - [PYSIDE-2854] libshiboken: The internal map instances has been changed + to be a multimap to improve support for co-located objects. + - [PYSIDE-3107] An automatic conversion from T to std::optional + has been added (in case std::optional is specified in + the type system). + - [PYSIDE-3138] A CMake package "Shiboken6Tools" has been introduced to make + integrating Shiboken with CMake easier. This significantly + reduces the amount of CMake code required by users, + replacing the previous method of calling the executable + directly with a Python script (`pyside_config.py`). + - [PYSIDE-3171] libshiboken's replacement functions providing functions + missing from the limited API or appearing in future Python + versions have been moved from sbkpython.h (providing a + sanitized Python.h) to separate headers sbkpep.h and + sbkpepbuffer.h (providing buffer API). This should not + affect binding code as the generator includes the new + headers, but may require adaption in client code using + libshiboken/libpyside directly. diff --git a/doc/changelogs/changes-6.10.1 b/doc/changelogs/changes-6.10.1 new file mode 100644 index 00000000..7d565d87 --- /dev/null +++ b/doc/changelogs/changes-6.10.1 @@ -0,0 +1,41 @@ +Qt for Python 6.10.1 is a bug-fix release. + +For more details, refer to the online documentation included in this +distribution. The documentation is also available online: + +https://doc.qt.io/qtforpython/ + +Some of the changes listed in this file include issue tracking numbers +corresponding to tasks in the Qt Bug Tracker: + +https://bugreports.qt.io/ + +Each of these identifiers can be entered in the bug tracker to obtain more +information about a particular change. + +**************************************************************************** +* PySide6 * +**************************************************************************** + + - [PYSIDE-2206] Many examples and tutorials have been updated. + - [PYSIDE-3147] Python 3.14 is now supported. + - [PYSIDE-3174] type hints: The type annotations of QtCore.Slot have been + fixed. + - [PYSIDE-3179] A regression breaking QtRemoteObjects has been fixed. + - [PYSIDE-3190] Disconnecting multiple index-based connections has been + fixed. + - [PYSIDE-3201] An issue handling types with equal names in signal/slot + connections has been fixed. + - [PYSIDE-3206] A conversion from tuple to QVariantList has been added. + - [PYSIDE-3213] QQuickTextDocument can now be used as a property in QML. + - [PYSIDE-3217] A regression breaking enumeration properties in Qt Widgets + Designer has been fixed. + - [PYSIDE-3227] A bug affecting QMetaProperty attributes when using + @Property has been fixed. + +**************************************************************************** +* Shiboken6 * +**************************************************************************** + + - [QTBUG-141204] An exclusion for a clang warning occurring when parsing + Qt code has been added (libclang v21). diff --git a/doc/changelogs/changes-6.10.2 b/doc/changelogs/changes-6.10.2 new file mode 100644 index 00000000..d9bb532d --- /dev/null +++ b/doc/changelogs/changes-6.10.2 @@ -0,0 +1,48 @@ +Qt for Python 6.10.2 is a bug-fix release. + +For more details, refer to the online documentation included in this +distribution. The documentation is also available online: + +https://doc.qt.io/qtforpython/ + +Some of the changes listed in this file include issue tracking numbers +corresponding to tasks in the Qt Bug Tracker: + +https://bugreports.qt.io/ + +Each of these identifiers can be entered in the bug tracker to obtain more +information about a particular change. + +**************************************************************************** +* PySide6 * +**************************************************************************** + + - [PYSIDE-2206] The multimedia player example has been updated. + - [PYSIDE-3189] Type hints: Some missing optional return types have been + added. + - [PYSIDE-3231] Type hints: Element access for all QMatrix types + has been fixed to consistently provide a call + operator (operator()) as well as mgetitem (operator[][]). + - [PYSIDE-3233] The Shiboken6Tools CMake module has been fixed to find + Python on manylinux_2_34. + - [PYSIDE-3241] The metatype JSON files that had been missing from the + wheels have been added. + - [PYSIDE-3248] QtWebView has been added to the Android wheels. + - [PYSIDE-3034] Type hints: Public variables have been added. + - [PYSIDE-3250] The snake_case feature has been fixed to also work + for imported modules. + - [PYSIDE-3254] The include-dir specification in the pkgconfig file + of libpyside has been fixed. + - [PYSIDE-3244] A regression causing tuples to be converted to + - [PYSIDE-3256] QVariantList when passed in a QVariant has been fixed. + +**************************************************************************** +* Shiboken6 * +**************************************************************************** + + - [PYSIDE-3235] Building for Yocto using a Clang toolchain has been fixed. + - [PYSIDE-3246] A crash occurring when multiple threads attempt to + retrieve the same method override has been fixed. + - [PYSIDE-3259] The conversion generated for std::vector has been + fixed for compilers that actually implement the + std::vector optimization (Clang). diff --git a/doc/changelogs/changes-6.9.3 b/doc/changelogs/changes-6.9.3 new file mode 100644 index 00000000..b0f55fcf --- /dev/null +++ b/doc/changelogs/changes-6.9.3 @@ -0,0 +1,40 @@ +Qt for Python 6.9.3 is a bug-fix release. + +For more details, refer to the online documentation included in this +distribution. The documentation is also available online: + +https://doc.qt.io/qtforpython/ + +Some of the changes listed in this file include issue tracking numbers +corresponding to tasks in the Qt Bug Tracker: + +https://bugreports.qt.io/ + +Each of these identifiers can be entered in the bug tracker to obtain more +information about a particular change. + +**************************************************************************** +* PySide6 * +**************************************************************************** + + - [PYSIDE-2308] type hints: The type annotation of the notify parameter of + QtCore.Property has been corrected. + - [PYSIDE-3047] type hints: The type annotations of QPolygon(F)'s + operator<< have been corrected. + - [PYSIDE-3048] type hints: The type annotations now contain enum values. + - [PYSIDE-3162] type hints: The 'from __future__ import annotations' have + been removed from the stub files. + - [PYSIDE-3163] type hints: The mypy comment has been removed from the + docstring. + - [PYSIDE-2206] Tye QtBluetooth/heartrate_game example has been updated. + +**************************************************************************** +* Shiboken6 * +**************************************************************************** + + - [PYSIDE-1106] DocGenerator: Extracting documentation from C++ structs + has been fixed. + - [PYSIDE-3173] A crash when encountering UTF-8 encoding errors has been + changed to a fatal error. + - [PYSIDE-3175] A crash occurring when the typesystem's package attribute + is missing has been fixed. diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/BluetoothAlarmDialog.qml b/examples/bluetooth/heartrate_game/HeartRateGame/BluetoothAlarmDialog.qml index 3687b133..16b4d32b 100644 --- a/examples/bluetooth/heartrate_game/HeartRateGame/BluetoothAlarmDialog.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/BluetoothAlarmDialog.qml @@ -52,7 +52,7 @@ Item { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter wrapMode: Text.WordWrap - font.pixelSize: GameSettings.mediumFontSize + font.pixelSize: GameSettings.smallFontSize color: GameSettings.textColor text: root.permissionError ? qsTr("Bluetooth permissions are not granted. Please grant the permissions in the system settings.") @@ -70,8 +70,8 @@ Item { Text { anchors.centerIn: parent color: GameSettings.textColor - font.pixelSize: GameSettings.bigFontSize - text: qsTr("Quit") + font.pixelSize: GameSettings.microFontSize + text: qsTr("QUIT") } } } diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/BottomLine.qml b/examples/bluetooth/heartrate_game/HeartRateGame/BottomLine.qml index caebc307..80fdaa8c 100644 --- a/examples/bluetooth/heartrate_game/HeartRateGame/BottomLine.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/BottomLine.qml @@ -6,7 +6,6 @@ import QtQuick Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - width: parent.width * 0.85 + width: parent.width height: parent.height * 0.05 - radius: height*0.5 } diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/Connect.qml b/examples/bluetooth/heartrate_game/HeartRateGame/Connect.qml index ca8ef292..ed5fb63d 100644 --- a/examples/bluetooth/heartrate_game/HeartRateGame/Connect.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/Connect.qml @@ -16,43 +16,39 @@ GamePage { errorMessage: deviceFinder.error infoMessage: deviceFinder.info + iconType: deviceFinder.icon + + Text { + id: viewCaption + anchors { + top: parent.top + topMargin: GameSettings.fieldMargin + connectPage.messageHeight + horizontalCenter: parent.horizontalCenter + } + width: parent.width - GameSettings.fieldMargin * 2 + height: GameSettings.fieldHeight + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + color: GameSettings.textColor + font.pixelSize: GameSettings.smallFontSize + text: qsTr("Found Devices") + } Rectangle { id: viewContainer - anchors.top: parent.top + anchors.top: viewCaption.bottom // only BlueZ platform has address type selection anchors.bottom: connectPage.connectionHandler.requiresAddressType ? addressTypeButton.top : searchButton.top - anchors.topMargin: GameSettings.fieldMargin + connectPage.messageHeight anchors.bottomMargin: GameSettings.fieldMargin anchors.horizontalCenter: parent.horizontalCenter width: parent.width - GameSettings.fieldMargin * 2 color: GameSettings.viewColor radius: GameSettings.buttonRadius - Text { - id: title - width: parent.width - height: GameSettings.fieldHeight - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: GameSettings.textColor - font.pixelSize: GameSettings.mediumFontSize - text: qsTr("FOUND DEVICES") - - BottomLine { - height: 1 - width: parent.width - color: "#898989" - } - } - ListView { id: devices - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.top: title.bottom + anchors.fill: parent model: connectPage.deviceFinder.devices clip: true @@ -76,22 +72,22 @@ GamePage { Text { id: device - font.pixelSize: GameSettings.smallFontSize + font.pixelSize: GameSettings.microFontSize text: box.modelData.deviceName anchors.top: parent.top - anchors.topMargin: parent.height * 0.1 - anchors.leftMargin: parent.height * 0.1 + anchors.topMargin: parent.height * 0.15 + anchors.leftMargin: parent.height * 0.15 anchors.left: parent.left color: GameSettings.textColor } Text { id: deviceAddress - font.pixelSize: GameSettings.smallFontSize + font.pixelSize: GameSettings.microFontSize text: box.modelData.deviceAddress anchors.bottom: parent.bottom - anchors.bottomMargin: parent.height * 0.1 - anchors.rightMargin: parent.height * 0.1 + anchors.bottomMargin: parent.height * 0.15 + anchors.rightMargin: parent.height * 0.15 anchors.right: parent.right color: Qt.darker(GameSettings.textColor) } @@ -114,19 +110,19 @@ GamePage { State { name: "public" PropertyChanges { - addressTypeText.text: qsTr("Public Address") + addressTypeText.text: qsTr("PUBLIC ADDRESS") } PropertyChanges { - connectPage.deviceHandler.addressType: DeviceHandler.PUBLIC_ADDRESS + connectPage.deviceHandler.addressType: DeviceHandler.PublicAddress } }, State { name: "random" PropertyChanges { - addressTypeText.text: qsTr("Random Address") + addressTypeText.text: qsTr("RANDOM ADDRESS") } PropertyChanges { - connectPage.deviceHandler.addressType: DeviceHandler.RANDOM_ADDRESS + connectPage.deviceHandler.addressType: DeviceHandler.RandomAddress } } ] @@ -134,8 +130,8 @@ GamePage { Text { id: addressTypeText anchors.centerIn: parent - font.pixelSize: GameSettings.tinyFontSize - color: GameSettings.textColor + font.pixelSize: GameSettings.microFontSize + color: GameSettings.textDarkColor } } @@ -151,9 +147,9 @@ GamePage { Text { anchors.centerIn: parent - font.pixelSize: GameSettings.tinyFontSize + font.pixelSize: GameSettings.microFontSize text: qsTr("START SEARCH") - color: searchButton.enabled ? GameSettings.textColor : GameSettings.disabledTextColor + color: GameSettings.textDarkColor } } } diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/GamePage.qml b/examples/bluetooth/heartrate_game/HeartRateGame/GamePage.qml index 249f9418..2d592cfd 100644 --- a/examples/bluetooth/heartrate_game/HeartRateGame/GamePage.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/GamePage.qml @@ -11,25 +11,65 @@ Item { property real messageHeight: msg.height property bool hasError: errorMessage != "" property bool hasInfo: infoMessage != "" + property int iconType: BluetoothBaseClass.IconNone + + function iconTypeToName(icon: int) : string { + switch (icon) { + case BluetoothBaseClass.IconNone: return "" + case BluetoothBaseClass.IconBluetooth: return "images/bluetooth.svg" + case BluetoothBaseClass.IconError: return "images/alert.svg" + case BluetoothBaseClass.IconProgress: return "images/progress.svg" + case BluetoothBaseClass.IconSearch: return "images/search.svg" + } + } Rectangle { id: msg - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + anchors { + top: parent.top + left: parent.left + right: parent.right + topMargin: GameSettings.fieldMargin * 0.5 + leftMargin: GameSettings.fieldMargin + rightMargin: GameSettings.fieldMargin + } height: GameSettings.fieldHeight - color: page.hasError ? GameSettings.errorColor : GameSettings.infoColor + radius: GameSettings.buttonRadius + color: page.hasError ? GameSettings.errorColor : "transparent" visible: page.hasError || page.hasInfo + border { + width: 1 + color: page.hasError ? GameSettings.errorColor : GameSettings.infoColor + } + + Image { + id: icon + readonly property int imgSize: GameSettings.fieldHeight * 0.5 + anchors { + left: parent.left + leftMargin: GameSettings.fieldMargin * 0.5 + verticalCenter: parent.verticalCenter + } + visible: source.toString() !== "" + source: page.iconTypeToName(page.iconType) + sourceSize.width: imgSize + sourceSize.height: imgSize + fillMode: Image.PreserveAspectFit + } Text { id: error - anchors.fill: parent + anchors { + fill: parent + leftMargin: GameSettings.fieldMargin + icon.width + rightMargin: GameSettings.fieldMargin + icon.width + } horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter minimumPixelSize: 5 - font.pixelSize: GameSettings.smallFontSize + font.pixelSize: GameSettings.microFontSize fontSizeMode: Text.Fit - color: GameSettings.textColor + color: page.hasError ? GameSettings.textColor : GameSettings.infoColor text: page.hasError ? page.errorMessage : page.infoMessage } } diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/GameSettings.qml b/examples/bluetooth/heartrate_game/HeartRateGame/GameSettings.qml index 0fe85460..4032787c 100644 --- a/examples/bluetooth/heartrate_game/HeartRateGame/GameSettings.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/GameSettings.qml @@ -4,35 +4,49 @@ pragma Singleton import QtQuick -Item { +QtObject { property int wHeight property int wWidth // Colors - readonly property color backgroundColor: "#2d3037" - readonly property color buttonColor: "#202227" - readonly property color buttonPressedColor: "#6ccaf2" - readonly property color disabledButtonColor: "#555555" - readonly property color viewColor: "#202227" - readonly property color delegate1Color: Qt.darker(viewColor, 1.2) - readonly property color delegate2Color: Qt.lighter(viewColor, 1.2) + readonly property color lightGreenColor: "#80ebb6" + readonly property color backgroundColor: "#2c3038" + readonly property color buttonColor: "#2cde85" + readonly property color buttonPressedColor: lightGreenColor + readonly property color disabledButtonColor: "#808080" + readonly property color viewColor: "#262626" + readonly property color delegate1Color: "#262626" + readonly property color delegate2Color: "#404040" readonly property color textColor: "#ffffff" - readonly property color textDarkColor: "#232323" - readonly property color disabledTextColor: "#777777" - readonly property color sliderColor: "#6ccaf2" + readonly property color textDarkColor: "#0d0d0d" + readonly property color textInfoColor: lightGreenColor + readonly property color sliderColor: "#00414a" + readonly property color sliderBorderColor: lightGreenColor + readonly property color sliderTextColor: lightGreenColor readonly property color errorColor: "#ba3f62" - readonly property color infoColor: "#3fba62" + readonly property color infoColor: lightGreenColor + readonly property color titleColor: "#202227" + readonly property color selectedTitleColor: "#19545c" + readonly property color hoverTitleColor: Qt.rgba(selectedTitleColor.r, + selectedTitleColor.g, + selectedTitleColor.b, + 0.25) + readonly property color bottomLineColor: "#e6e6e6" + readonly property color heartRateColor: "#f80067" + + // All the fonts are given for the window of certain size. + // Resizing the window changes all the fonts accordingly + readonly property int defaultSize: 500 + readonly property real fontScaleFactor: Math.min(wWidth, wHeight) / defaultSize // Font sizes - property real microFontSize: hugeFontSize * 0.2 - property real tinyFontSize: hugeFontSize * 0.4 - property real smallTinyFontSize: hugeFontSize * 0.5 - property real smallFontSize: hugeFontSize * 0.6 - property real mediumFontSize: hugeFontSize * 0.7 - property real bigFontSize: hugeFontSize * 0.8 - property real largeFontSize: hugeFontSize * 0.9 - property real hugeFontSize: (wWidth + wHeight) * 0.03 - property real giganticFontSize: (wWidth + wHeight) * 0.04 + readonly property real microFontSize: 16 * fontScaleFactor + readonly property real tinyFontSize: 20 * fontScaleFactor + readonly property real smallFontSize: 24 * fontScaleFactor + readonly property real mediumFontSize: 32 * fontScaleFactor + readonly property real bigFontSize: 36 * fontScaleFactor + readonly property real largeFontSize: 54 * fontScaleFactor + readonly property real hugeFontSize: 128 * fontScaleFactor // Some other values property real fieldHeight: wHeight * 0.08 @@ -41,10 +55,6 @@ Item { property real buttonRadius: buttonHeight * 0.1 // Some help functions - function widthForHeight(h, ss) { - return h / ss.height * ss.width - } - function heightForWidth(w, ss) { return w / ss.width * ss.height } diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/Measure.qml b/examples/bluetooth/heartrate_game/HeartRateGame/Measure.qml index 48e84e76..04ebeb09 100644 --- a/examples/bluetooth/heartrate_game/HeartRateGame/Measure.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/Measure.qml @@ -11,10 +11,15 @@ GamePage { errorMessage: deviceHandler.error infoMessage: deviceHandler.info + iconType: deviceHandler.icon property real __timeCounter: 0 property real __maxTimeCount: 60 - property string relaxText: qsTr("Relax!\nWhen you are ready, press Start. You have %1s time to increase heartrate so much as possible.\nGood luck!").arg(__maxTimeCount) + + readonly property string relaxText: qsTr("Relax!") + readonly property string startText: qsTr("When you are ready,\npress Start.") + readonly property string instructionText: qsTr("You have %1s time to increase heart\nrate as much as possible.").arg(__maxTimeCount) + readonly property string goodLuckText: qsTr("Good luck!") signal showStatsPage @@ -55,6 +60,10 @@ GamePage { Rectangle { id: circle + + readonly property bool hintVisible: !measurePage.deviceHandler.measuring + readonly property real innerSpacing: Math.min(width * 0.05, 25) + anchors.horizontalCenter: parent.horizontalCenter width: Math.min(measurePage.width, measurePage.height - GameSettings.fieldHeight * 4) - 2 * GameSettings.fieldMargin @@ -63,30 +72,127 @@ GamePage { color: GameSettings.viewColor Text { - id: hintText - anchors.centerIn: parent - anchors.verticalCenterOffset: -parent.height * 0.1 + id: relaxTextBox + anchors { + bottom: startTextBox.top + bottomMargin: parent.innerSpacing + horizontalCenter: parent.horizontalCenter + } + width: parent.width * 0.6 + height: parent.height * 0.1 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - width: parent.width * 0.8 - height: parent.height * 0.6 - wrapMode: Text.WordWrap text: measurePage.relaxText - visible: !measurePage.deviceHandler.measuring + visible: circle.hintVisible color: GameSettings.textColor fontSizeMode: Text.Fit - minimumPixelSize: 10 - font.pixelSize: GameSettings.mediumFontSize + font.pixelSize: GameSettings.smallFontSize + font.bold: true } Text { - id: text - anchors.centerIn: parent - anchors.verticalCenterOffset: -parent.height * 0.15 - font.pixelSize: parent.width * 0.45 + id: startTextBox + anchors { + bottom: heart.top + bottomMargin: parent.innerSpacing + horizontalCenter: parent.horizontalCenter + } + width: parent.width * 0.8 + height: parent.height * 0.15 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: measurePage.startText + visible: circle.hintVisible + color: GameSettings.textColor + fontSizeMode: Text.Fit + font.pixelSize: GameSettings.tinyFontSize + } + + Text { + id: measureTextBox + anchors { + bottom: heart.top + horizontalCenter: parent.horizontalCenter + } + width: parent.width * 0.7 + height: parent.height * 0.35 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter text: measurePage.deviceHandler.hr visible: measurePage.deviceHandler.measuring + color: GameSettings.heartRateColor + fontSizeMode: Text.Fit + font.pixelSize: GameSettings.hugeFontSize + font.bold: true + } + + Image { + id: heart + anchors.centerIn: circle + width: parent.width * 0.2 + height: width + fillMode: Image.PreserveAspectFit + source: "images/heart.png" + smooth: true + antialiasing: true + + SequentialAnimation { + id: heartAnim + running: measurePage.deviceHandler.measuring + loops: Animation.Infinite + alwaysRunToEnd: true + PropertyAnimation { + target: heart + property: "scale" + to: 1.4 + duration: 500 + easing.type: Easing.InQuad + } + PropertyAnimation { + target: heart + property: "scale" + to: 1.0 + duration: 500 + easing.type: Easing.OutQuad + } + } + } + + Text { + id: instructionTextBox + anchors { + top: heart.bottom + topMargin: parent.innerSpacing + horizontalCenter: parent.horizontalCenter + } + width: parent.width * 0.8 + height: parent.height * 0.15 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: measurePage.instructionText + visible: circle.hintVisible color: GameSettings.textColor + fontSizeMode: Text.Fit + font.pixelSize: GameSettings.tinyFontSize + } + + Text { + id: goodLuckBox + anchors { + top: instructionTextBox.bottom + topMargin: parent.innerSpacing + horizontalCenter: parent.horizontalCenter + } + width: parent.width * 0.6 + height: parent.height * 0.1 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: measurePage.goodLuckText + visible: circle.hintVisible + color: GameSettings.textColor + fontSizeMode: Text.Fit + font.pixelSize: GameSettings.smallFontSize + font.bold: true } Item { @@ -101,14 +207,22 @@ GamePage { Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter + width: parent.width * 0.35 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter text: measurePage.deviceHandler.minHR color: GameSettings.textColor - font.pixelSize: GameSettings.hugeFontSize + fontSizeMode: Text.Fit + font.pixelSize: GameSettings.largeFontSize Text { anchors.left: parent.left anchors.bottom: parent.top - font.pixelSize: parent.font.pixelSize * 0.8 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + width: parent.width + fontSizeMode: Text.Fit + font.pixelSize: GameSettings.mediumFontSize color: parent.color text: "MIN" } @@ -117,51 +231,27 @@ GamePage { Text { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + width: parent.width * 0.35 text: measurePage.deviceHandler.maxHR color: GameSettings.textColor - font.pixelSize: GameSettings.hugeFontSize + fontSizeMode: Text.Fit + font.pixelSize: GameSettings.largeFontSize Text { anchors.right: parent.right anchors.bottom: parent.top - font.pixelSize: parent.font.pixelSize * 0.8 + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + width: parent.width + fontSizeMode: Text.Fit + font.pixelSize: GameSettings.mediumFontSize color: parent.color text: "MAX" } } } - - Image { - id: heart - anchors.horizontalCenter: minMaxContainer.horizontalCenter - anchors.verticalCenter: minMaxContainer.bottom - width: parent.width * 0.2 - height: width - source: "images/heart.png" - smooth: true - antialiasing: true - - SequentialAnimation { - id: heartAnim - running: measurePage.deviceHandler.alive - loops: Animation.Infinite - alwaysRunToEnd: true - PropertyAnimation { - target: heart - property: "scale" - to: 1.2 - duration: 500 - easing.type: Easing.InQuad - } - PropertyAnimation { - target: heart - property: "scale" - to: 1.0 - duration: 500 - easing.type: Easing.OutQuad - } - } - } } Rectangle { @@ -171,21 +261,43 @@ GamePage { width: circle.width height: GameSettings.fieldHeight radius: GameSettings.buttonRadius + border { + width: 1 + color: GameSettings.sliderBorderColor + } Rectangle { - height: parent.height + anchors { + top: parent.top + topMargin: parent.border.width + left: parent.left + leftMargin: parent.border.width + } + height: parent.height - 2 * parent.border.width + width: Math.min(1.0, measurePage.__timeCounter / measurePage.__maxTimeCount) + * (parent.width - 2 * parent.border.width) radius: parent.radius color: GameSettings.sliderColor - width: Math.min( - 1.0, - measurePage.__timeCounter / measurePage.__maxTimeCount) * parent.width + } + + Image { + readonly property int imgSize: GameSettings.fieldHeight * 0.5 + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: GameSettings.fieldMargin * 0.5 + } + source: "images/clock.svg" + sourceSize.width: imgSize + sourceSize.height: imgSize + fillMode: Image.PreserveAspectFit } Text { anchors.centerIn: parent - color: "gray" + color: GameSettings.sliderTextColor text: (measurePage.__maxTimeCount - measurePage.__timeCounter).toFixed(0) + " s" - font.pixelSize: GameSettings.bigFontSize + font.pixelSize: GameSettings.smallFontSize } } } @@ -197,16 +309,17 @@ GamePage { anchors.bottomMargin: GameSettings.fieldMargin width: circle.width height: GameSettings.fieldHeight - enabled: !measurePage.deviceHandler.measuring + enabled: measurePage.deviceHandler.alive && !measurePage.deviceHandler.measuring + && measurePage.errorMessage === "" radius: GameSettings.buttonRadius onClicked: measurePage.start() Text { anchors.centerIn: parent - font.pixelSize: GameSettings.tinyFontSize + font.pixelSize: GameSettings.microFontSize text: qsTr("START") - color: startButton.enabled ? GameSettings.textColor : GameSettings.disabledTextColor + color: GameSettings.textDarkColor } } } diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/SplashScreen.qml b/examples/bluetooth/heartrate_game/HeartRateGame/SplashScreen.qml index 2f9ac1b3..918319d7 100644 --- a/examples/bluetooth/heartrate_game/HeartRateGame/SplashScreen.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/SplashScreen.qml @@ -23,7 +23,7 @@ Item { Timer { id: splashTimer interval: 1000 - onTriggered: splashIsReady = true + onTriggered: root.splashIsReady = true } Component.onCompleted: splashTimer.start() diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/Stats.qml b/examples/bluetooth/heartrate_game/HeartRateGame/Stats.qml index 22cdd536..87487c94 100644 --- a/examples/bluetooth/heartrate_game/HeartRateGame/Stats.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/Stats.qml @@ -13,20 +13,45 @@ GamePage { anchors.centerIn: parent width: parent.width - Text { + Rectangle { + id: resultRect anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: GameSettings.hugeFontSize - color: GameSettings.textColor - text: qsTr("RESULT") - } + width: height + height: statsPage.height / 2 - GameSettings.fieldHeight + radius: height / 2 + color: GameSettings.viewColor - Text { - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: GameSettings.giganticFontSize * 3 - color: GameSettings.textColor - text: (statsPage.deviceHandler.maxHR - statsPage.deviceHandler.minHR).toFixed(0) + Column { + anchors.centerIn: parent + + Text { + id: resultCaption + anchors.horizontalCenter: parent.horizontalCenter + width: resultRect.width * 0.8 + height: resultRect.height * 0.15 + horizontalAlignment: Text.AlignHCenter + fontSizeMode: Text.Fit + font.pixelSize: GameSettings.bigFontSize + color: GameSettings.textColor + text: qsTr("RESULT") + } + + Text { + id: resultValue + anchors.horizontalCenter: parent.horizontalCenter + width: resultRect.width * 0.8 + height: resultRect.height * 0.4 + horizontalAlignment: Text.AlignHCenter + fontSizeMode: Text.Fit + font.pixelSize: GameSettings.hugeFontSize + font.bold: true + color: GameSettings.heartRateColor + text: (statsPage.deviceHandler.maxHR - statsPage.deviceHandler.minHR).toFixed(0) + } + } } + Item { height: GameSettings.fieldHeight width: 1 diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/TitleBar.qml b/examples/bluetooth/heartrate_game/HeartRateGame/TitleBar.qml index 016a4435..ccec7608 100644 --- a/examples/bluetooth/heartrate_game/HeartRateGame/TitleBar.qml +++ b/examples/bluetooth/heartrate_game/HeartRateGame/TitleBar.qml @@ -13,42 +13,51 @@ Rectangle { signal titleClicked(int index) height: GameSettings.fieldHeight - color: GameSettings.viewColor + color: GameSettings.titleColor + + Rectangle { + anchors.bottom: parent.bottom + width: parent.width / 3 + height: parent.height + x: titleBar.currentIndex * width + color: GameSettings.selectedTitleColor + + BottomLine { + color: GameSettings.bottomLineColor + } + + Behavior on x { + NumberAnimation { + duration: 200 + } + } + } Repeater { model: 3 - Text { + Rectangle { id: caption required property int index + property bool hoveredOrPressed: mouseArea.pressed || mouseArea.containsMouse width: titleBar.width / 3 height: titleBar.height x: index * width - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: titleBar.__titles[index] - font.pixelSize: GameSettings.tinyFontSize - color: titleBar.currentIndex === index ? GameSettings.textColor - : GameSettings.disabledTextColor - + color: (titleBar.currentIndex !== index) && hoveredOrPressed + ? GameSettings.hoverTitleColor : "transparent" + Text { + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: titleBar.__titles[caption.index] + font.pixelSize: GameSettings.microFontSize + color: GameSettings.textColor + } MouseArea { + id: mouseArea anchors.fill: parent + hoverEnabled: true onClicked: titleBar.titleClicked(caption.index) } } } - - Item { - anchors.bottom: parent.bottom - width: parent.width / 3 - height: parent.height - x: titleBar.currentIndex * width - - BottomLine {} - - Behavior on x { - NumberAnimation { - duration: 200 - } - } - } } diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/images/alert.svg b/examples/bluetooth/heartrate_game/HeartRateGame/images/alert.svg new file mode 100644 index 00000000..c48c10e6 --- /dev/null +++ b/examples/bluetooth/heartrate_game/HeartRateGame/images/alert.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/images/bluetooth.svg b/examples/bluetooth/heartrate_game/HeartRateGame/images/bluetooth.svg new file mode 100644 index 00000000..6d01b28f --- /dev/null +++ b/examples/bluetooth/heartrate_game/HeartRateGame/images/bluetooth.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/images/clock.svg b/examples/bluetooth/heartrate_game/HeartRateGame/images/clock.svg new file mode 100644 index 00000000..655996ba --- /dev/null +++ b/examples/bluetooth/heartrate_game/HeartRateGame/images/clock.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/images/heart.png b/examples/bluetooth/heartrate_game/HeartRateGame/images/heart.png index f2b3c0a3..4ba0f822 100644 Binary files a/examples/bluetooth/heartrate_game/HeartRateGame/images/heart.png and b/examples/bluetooth/heartrate_game/HeartRateGame/images/heart.png differ diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/images/progress.svg b/examples/bluetooth/heartrate_game/HeartRateGame/images/progress.svg new file mode 100644 index 00000000..449fe5e7 --- /dev/null +++ b/examples/bluetooth/heartrate_game/HeartRateGame/images/progress.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/bluetooth/heartrate_game/HeartRateGame/images/search.svg b/examples/bluetooth/heartrate_game/HeartRateGame/images/search.svg new file mode 100644 index 00000000..9af5fe4d --- /dev/null +++ b/examples/bluetooth/heartrate_game/HeartRateGame/images/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/bluetooth/heartrate_game/bluetoothbaseclass.py b/examples/bluetooth/heartrate_game/bluetoothbaseclass.py index 6278b041..7f4c5a5b 100644 --- a/examples/bluetooth/heartrate_game/bluetoothbaseclass.py +++ b/examples/bluetooth/heartrate_game/bluetoothbaseclass.py @@ -2,18 +2,36 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause from __future__ import annotations -from PySide6.QtCore import QObject, Property, Signal, Slot +from enum import IntEnum +from PySide6.QtQml import QmlElement, QmlUncreatable +from PySide6.QtCore import QObject, Property, Signal, Slot, QEnum +QML_IMPORT_NAME = "HeartRateGame" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +@QmlUncreatable("BluetoothBaseClass is not intended to be created directly") class BluetoothBaseClass(QObject): + @QEnum + class IconType(IntEnum): + IconNone = 0 + IconBluetooth = 1 + IconError = 2 + IconProgress = 3 + IconSearch = 4 + errorChanged = Signal() infoChanged = Signal() + iconChanged = Signal() def __init__(self, parent=None): super().__init__(parent) self.m_error = "" self.m_info = "" + self.m_icon = BluetoothBaseClass.IconType.IconNone @Property(str, notify=errorChanged) def error(self): @@ -35,7 +53,18 @@ class BluetoothBaseClass(QObject): self.m_info = i self.infoChanged.emit() + @Property(int, notify=iconChanged) + def icon(self): + return self.m_icon + + @icon.setter + def icon(self, i): + if self.m_icon != i: + self.m_icon = i + self.iconChanged.emit() + @Slot() def clearMessages(self): self.info = "" self.error = "" + self.icon = BluetoothBaseClass.IconType.IconNone diff --git a/examples/bluetooth/heartrate_game/devicefinder.py b/examples/bluetooth/heartrate_game/devicefinder.py index 5c00e7c9..932f2bb4 100644 --- a/examples/bluetooth/heartrate_game/devicefinder.py +++ b/examples/bluetooth/heartrate_game/devicefinder.py @@ -5,7 +5,7 @@ import sys from PySide6.QtBluetooth import (QBluetoothDeviceDiscoveryAgent, QBluetoothDeviceInfo) -from PySide6.QtQml import QmlElement +from PySide6.QtQml import QmlElement, QmlUncreatable from PySide6.QtCore import QTimer, Property, Signal, Slot, Qt from bluetoothbaseclass import BluetoothBaseClass @@ -22,6 +22,7 @@ QML_IMPORT_MAJOR_VERSION = 1 @QmlElement +@QmlUncreatable("This class is not intended to be created directly") class DeviceFinder(BluetoothBaseClass): scanningChanged = Signal() @@ -57,6 +58,7 @@ class DeviceFinder(BluetoothBaseClass): qApp.requestPermission(permission, self, self.startSearch) # noqa: F82 1 return elif permission_status == Qt.PermissionStatus.Denied: + self.icon = BluetoothBaseClass.IconType.IconError return elif permission_status == Qt.PermissionStatus.Granted: print("[HeartRateGame] Bluetooth Permission Granted") @@ -75,6 +77,7 @@ class DeviceFinder(BluetoothBaseClass): #! [devicediscovery-2] self.scanningChanged.emit() self.info = "Scanning for devices..." + self.icon = BluetoothBaseClass.IconType.IconProgress #! [devicediscovery-3] @Slot(QBluetoothDeviceInfo) @@ -83,6 +86,7 @@ class DeviceFinder(BluetoothBaseClass): if device.coreConfigurations() & QBluetoothDeviceInfo.LowEnergyCoreConfiguration: self.m_devices.append(DeviceInfo(device)) self.info = "Low Energy device found. Scanning more..." + self.icon = BluetoothBaseClass.IconType.IconProgress #! [devicediscovery-3] self.devicesChanged.emit() #! [devicediscovery-4] @@ -97,6 +101,7 @@ class DeviceFinder(BluetoothBaseClass): self.error = "Writing or reading from the device resulted in an error." else: self.error = "An unknown error has occurred." + self.icon = BluetoothBaseClass.IconType.IconError @Slot() def scanFinished(self): @@ -107,12 +112,20 @@ class DeviceFinder(BluetoothBaseClass): if self.m_devices: self.info = "Scanning done." + self.icon = BluetoothBaseClass.IconType.IconBluetooth else: self.error = "No Low Energy devices found." + self.icon = BluetoothBaseClass.IconType.IconError self.scanningChanged.emit() self.devicesChanged.emit() + @Slot() + def resetMessages(self): + self.error = "" + self.info = "Start search to find devices" + self.icon = BluetoothBaseClass.IconType.IconSearch + @Slot(str) def connectToService(self, address): self.m_deviceDiscoveryAgent.stop() @@ -127,7 +140,7 @@ class DeviceFinder(BluetoothBaseClass): if currentDevice: self.m_deviceHandler.setDevice(currentDevice) - self.clearMessages() + self.resetMessages() @Property(bool, notify=scanningChanged) def scanning(self): diff --git a/examples/bluetooth/heartrate_game/devicehandler.py b/examples/bluetooth/heartrate_game/devicehandler.py index 8599c029..f10c052b 100644 --- a/examples/bluetooth/heartrate_game/devicehandler.py +++ b/examples/bluetooth/heartrate_game/devicehandler.py @@ -69,31 +69,37 @@ class DeviceHandler(BluetoothBaseClass): self.m_demoTimer.start() self.updateDemoHR() - @Property(int) def addressType(self): if self.m_addressType == QLowEnergyController.RemoteAddressType.RandomAddress: return DeviceHandler.AddressType.RANDOM_ADDRESS return DeviceHandler.AddressType.PUBLIC_ADDRESS - @addressType.setter - def addressType(self, type): + @Slot(int) + def setAddressType(self, type): if type == DeviceHandler.AddressType.PUBLIC_ADDRESS: self.m_addressType = QLowEnergyController.RemoteAddressType.PublicAddress elif type == DeviceHandler.AddressType.RANDOM_ADDRESS: self.m_addressType = QLowEnergyController.RemoteAddressType.RandomAddress + @Slot() + def resetAddressType(self): + self.m_addressType = QLowEnergyController.RemoteAddressType.PublicAddress + @Slot(QLowEnergyController.Error) def controllerErrorOccurred(self, device): self.error = "Cannot connect to remote device." + self.icon = BluetoothBaseClass.IconType.IconError @Slot() def controllerConnected(self): self.info = "Controller connected. Search services..." + self.icon = BluetoothBaseClass.IconType.IconProgress self.m_control.discoverServices() @Slot() def controllerDisconnected(self): self.error = "LowEnergy controller disconnected" + self.icon = BluetoothBaseClass.IconType.IconError def setDevice(self, device): self.clearMessages() @@ -101,6 +107,7 @@ class DeviceHandler(BluetoothBaseClass): if simulator(): self.info = "Demo device connected." + self.icon = BluetoothBaseClass.IconType.IconBluetooth return # Disconnect and delete old connection @@ -152,6 +159,7 @@ class DeviceHandler(BluetoothBaseClass): def serviceDiscovered(self, gatt): if gatt == QBluetoothUuid(QBluetoothUuid.ServiceClassUuid.HeartRate): self.info = "Heart Rate service discovered. Waiting for service scan to be done..." + self.icon = BluetoothBaseClass.IconType.IconProgress self.m_foundHeartRateService = True #! [Filter HeartRate service 1] @@ -159,6 +167,7 @@ class DeviceHandler(BluetoothBaseClass): @Slot() def serviceScanDone(self): self.info = "Service scan done." + self.icon = BluetoothBaseClass.IconType.IconProgress # Delete old service if available if self.m_service: @@ -177,6 +186,8 @@ class DeviceHandler(BluetoothBaseClass): self.m_service.discoverDetails() else: self.error = "Heart Rate Service not found." + self.icon = BluetoothBaseClass.IconType.IconError + #! [Filter HeartRate service 2] # Service functions @@ -185,8 +196,10 @@ class DeviceHandler(BluetoothBaseClass): def serviceStateChanged(self, switch): if switch == QLowEnergyService.RemoteServiceDiscovering: self.info = "Discovering services..." + self.icon = BluetoothBaseClass.IconType.IconProgress elif switch == QLowEnergyService.RemoteServiceDiscovered: self.info = "Service discovered." + self.icon = BluetoothBaseClass.IconType.IconBluetooth hrChar = self.m_service.characteristic( QBluetoothUuid(QBluetoothUuid.CharacteristicType.HeartRateMeasurement)) if hrChar.isValid(): @@ -197,6 +210,7 @@ class DeviceHandler(BluetoothBaseClass): QByteArray.fromHex(b"0100")) else: self.error = "HR Data not found." + self.icon = BluetoothBaseClass.IconType.IconError self.aliveChanged.emit() #! [Find HRM characteristic] @@ -308,3 +322,5 @@ class DeviceHandler(BluetoothBaseClass): + (0.2017 * 24)) / 4.184) * 60 * self.time / 3600 self.statsChanged.emit() + + addressType = Property(int, addressType, setAddressType, freset=resetAddressType) diff --git a/examples/bluetooth/heartrate_game/deviceinfo.py b/examples/bluetooth/heartrate_game/deviceinfo.py index 136bbbac..60cdb5aa 100644 --- a/examples/bluetooth/heartrate_game/deviceinfo.py +++ b/examples/bluetooth/heartrate_game/deviceinfo.py @@ -27,7 +27,7 @@ class DeviceInfo(QObject): @Property(str, notify=deviceChanged) def deviceName(self): if simulator(): - return "Demo device" + return "Demo BT device" return self.m_device.name() @Property(str, notify=deviceChanged) diff --git a/examples/bluetooth/heartrate_game/doc/heartrate_game.rst b/examples/bluetooth/heartrate_game/doc/heartrate_game.rst index 9d190d99..1704e32d 100644 --- a/examples/bluetooth/heartrate_game/doc/heartrate_game.rst +++ b/examples/bluetooth/heartrate_game/doc/heartrate_game.rst @@ -9,3 +9,6 @@ application covers the scanning for Bluetooth Low Energy devices, connecting to a Heart Rate service on the device, writing characteristics and descriptors, and receiving updates from the device once the heart rate has changed. + +The command line option `--simulator` can be used to run the example against a +demo server in case no Bluetooth hardware is available. diff --git a/examples/bluetooth/heartrate_game/heartrate_global.py b/examples/bluetooth/heartrate_game/heartrate_global.py index 384eb93a..75a0b42b 100644 --- a/examples/bluetooth/heartrate_game/heartrate_global.py +++ b/examples/bluetooth/heartrate_game/heartrate_global.py @@ -8,7 +8,6 @@ _simulator = False def simulator(): - global _simulator return _simulator diff --git a/examples/bluetooth/heartrate_game/main.py b/examples/bluetooth/heartrate_game/main.py index 00df7de6..a70b9f34 100644 --- a/examples/bluetooth/heartrate_game/main.py +++ b/examples/bluetooth/heartrate_game/main.py @@ -15,6 +15,7 @@ from PySide6.QtCore import QCoreApplication, QLoggingCategory from connectionhandler import ConnectionHandler from devicefinder import DeviceFinder from devicehandler import DeviceHandler +from bluetoothbaseclass import BluetoothBaseClass # noqa: F401 from heartrate_global import set_simulator diff --git a/examples/bluetooth/lowenergyscanner/Scanner/Services.qml b/examples/bluetooth/lowenergyscanner/Scanner/Services.qml index 70326242..ec0d599b 100644 --- a/examples/bluetooth/lowenergyscanner/Scanner/Services.qml +++ b/examples/bluetooth/lowenergyscanner/Scanner/Services.qml @@ -39,14 +39,14 @@ Rectangle { Connections { target: Device - function onservices_updated() { + function onServices_updated() { if (servicesview.count === 0) info.dialogText = "No services found" else info.visible = false } - function ondisconnected() { + function onDisconnected() { servicesPage.showDevices() } } diff --git a/examples/bluetooth/lowenergyscanner/device.py b/examples/bluetooth/lowenergyscanner/device.py index afa30ff8..5ae701fd 100644 --- a/examples/bluetooth/lowenergyscanner/device.py +++ b/examples/bluetooth/lowenergyscanner/device.py @@ -82,7 +82,8 @@ class Device(QObject): @Property(bool) def controller_error(self): - return self.controller and (self.controller.error() != QLowEnergyController.NoError) + return (self.controller is not None + and (self.controller.error() != QLowEnergyController.NoError)) @Slot() def start_device_discovery(self): diff --git a/examples/charts/callout/callout.py b/examples/charts/callout/callout.py index d8ba8e6e..c61e955c 100644 --- a/examples/charts/callout/callout.py +++ b/examples/charts/callout/callout.py @@ -82,6 +82,7 @@ class Callout(QGraphicsItem): path.lineTo(point2) path = path.simplified() + painter.setPen(QColor(0, 0, 0)) painter.setBrush(QColor(255, 255, 255)) painter.drawPath(path) painter.drawText(self._textRect, self._text) @@ -183,7 +184,7 @@ class View(QGraphicsView): self._chart.size().width() / 2 + 50, self._chart.size().height() - 20) for callout in self._callouts: - callout.updateGeometry() + callout.update_geometry() QGraphicsView.resizeEvent(self, event) def mouseMoveEvent(self, event): diff --git a/examples/corelib/mimetypesbrowser/mimetypesbrowser.py b/examples/corelib/mimetypesbrowser/mimetypesbrowser.py index 4f7b6959..00d58bde 100644 --- a/examples/corelib/mimetypesbrowser/mimetypesbrowser.py +++ b/examples/corelib/mimetypesbrowser/mimetypesbrowser.py @@ -8,10 +8,15 @@ import argparse import sys from mainwindow import MainWindow +from PySide6.QtCore import QLibraryInfo, QLocale, QTranslator from PySide6.QtWidgets import QApplication if __name__ == "__main__": app = QApplication(sys.argv) + translator = QTranslator(app) + if translator.load(QLocale.system(), "qtbase", "_", + QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)): + app.installTranslator(translator) parser = argparse.ArgumentParser(description="MimeTypesBrowser Example") parser.add_argument("-v", "--version", action="version", version="%(prog)s 1.0") diff --git a/examples/demos/colorpaletteclient/restservice.py b/examples/demos/colorpaletteclient/restservice.py index 852c50fe..076c56e3 100644 --- a/examples/demos/colorpaletteclient/restservice.py +++ b/examples/demos/colorpaletteclient/restservice.py @@ -12,6 +12,19 @@ QML_IMPORT_NAME = "ColorPalette" QML_IMPORT_MAJOR_VERSION = 1 +class ApiKeyRequestFactory(QNetworkRequestFactory): + """Custom request factory that adds the reqres.in API key to all requests""" + + def createRequest(self, path, query=None): + """Override to add API key header to every request""" + if query is None: + request = super().createRequest(path) + else: + request = super().createRequest(path, query) + request.setRawHeader(b"x-api-key", b"reqres-free-v1") + return request + + @QmlElement @ClassInfo(DefaultProperty="resources") class RestService(QPyQmlParserStatus): @@ -24,7 +37,7 @@ class RestService(QPyQmlParserStatus): self.m_qnam = QNetworkAccessManager() self.m_qnam.setAutoDeleteReplies(True) self.m_manager = QRestAccessManager(self.m_qnam) - self.m_serviceApi = QNetworkRequestFactory() + self.m_serviceApi = ApiKeyRequestFactory() @Property(str, notify=urlChanged) def url(self): diff --git a/examples/examples.pyproject b/examples/examples.pyproject index 814aae02..1a702aba 100644 --- a/examples/examples.pyproject +++ b/examples/examples.pyproject @@ -1,10 +1,34 @@ { "files": ["3d/simple3d/simple3d.py", + "async/eratosthenes/eratosthenes_asyncio.py", + "async/eratosthenes/eratosthenes_trio.py", + "async/minimal/minimal_asyncio.py", + "async/minimal/minimal_trio.py", "axcontainer/axviewer/axviewer.py", + "bluetooth/btscanner/main.py", + "bluetooth/btscanner/device.py", + "bluetooth/btscanner/service.py", + "bluetooth/heartrate_game/main.py", + "bluetooth/heartrate_game/bluetoothbaseclass.py", + "bluetooth/heartrate_game/connectionhandler.py", + "bluetooth/heartrate_game/devicefinder.py", + "bluetooth/heartrate_game/devicehandler.py", + "bluetooth/heartrate_game/deviceinfo.py", + "bluetooth/heartrate_game/heartrate_global.py", + "bluetooth/heartrate_server/heartrate_server.py", + "bluetooth/lowenergyscanner/main.py", + "bluetooth/lowenergyscanner/characteristicinfo.py", + "bluetooth/lowenergyscanner/device.py", + "bluetooth/lowenergyscanner/deviceinfo.py", + "bluetooth/lowenergyscanner/serviceinfo.py", + "charts/areachart/areachart.py", "charts/audio/audio.py", + "charts/barchart/barchart.py", "charts/callout/callout.py", "charts/chartthemes/main.py", "charts/donutbreakdown/donutbreakdown.py", + "charts/dynamicspline/dynamicspline.py", + "charts/dynamicspline/main.py", "charts/legend/legend.py", "charts/lineandbar/lineandbar.py", "charts/linechart/linechart.py", @@ -14,72 +38,246 @@ "charts/nesteddonuts/nesteddonuts.py", "charts/percentbarchart/percentbarchart.py", "charts/piechart/piechart.py", + "charts/pointconfiguration/chartwindow.py", + "charts/pointconfiguration/pointconfiguration.py", + "charts/pointselectionandmarkers/pointselectionandmarkers.py", + "charts/pointselectionandmarkers/utilities.py", "charts/qmlpolarchart/qmlpolarchart.py", "charts/temperaturerecords/temperaturerecords.py", + "charts/zoomlinechart/chart.py", + "charts/zoomlinechart/chartview.py", + "charts/zoomlinechart/main.py", + "corelib/ipc/sharedmemory/dialog.py", + "corelib/ipc/sharedmemory/main.py", + "corelib/mimetypesbrowser/mainwindow.py", + "corelib/mimetypesbrowser/mimetypemodel.py", + "corelib/mimetypesbrowser/mimetypesbrowser.py", "corelib/settingseditor/settingseditor.py", "corelib/threads/mandelbrot.py", "datavisualization/bars3d/bars3d.py", - "declarative/extending/chapter1-basics/basics.py", - "declarative/extending/chapter2-methods/methods.py", - "declarative/extending/chapter3-bindings/bindings.py", - "declarative/extending/chapter4-customPropertyTypes/customPropertyTypes.py", - "declarative/extending/chapter5-listproperties/listproperties.py", - "declarative/scrolling/scrolling.py", - "declarative/signals/pytoqml1/main.py", - "declarative/signals/pytoqml2/main.py", - "declarative/signals/qmltopy1/main.py", - "declarative/signals/qmltopy2/main.py", - "declarative/signals/qmltopy3/main.py", - "declarative/signals/qmltopy4/main.py", - "declarative/textproperties/main.py", - "declarative/usingmodel/usingmodel.py", + "datavisualization/graphgallery/axesinputhandler.py", + "datavisualization/graphgallery/bargraph.py", + "datavisualization/graphgallery/custominputhandler.py", + "datavisualization/graphgallery/graphmodifier.py", + "datavisualization/graphgallery/highlightseries.py", + "datavisualization/graphgallery/main.py", + "datavisualization/graphgallery/rainfalldata.py", + "datavisualization/graphgallery/scatterdatamodifier.py", + "datavisualization/graphgallery/scattergraph.py", + "datavisualization/graphgallery/surfacegraph.py", + "datavisualization/graphgallery/surfacegraphmodifier.py", + "datavisualization/graphgallery/topographicseries.py", + "datavisualization/graphgallery/variantbardatamapping.py", + "datavisualization/graphgallery/variantbardataproxy.py", + "datavisualization/graphgallery/variantdataset.py", + "datavisualization/minimalsurface/main.py", + "datavisualization/qmlsurfacegallery/datasource.py", + "datavisualization/qmlsurfacegallery/main.py", + "datavisualization/surface/main.py", + "datavisualization/surface/surfacegraph.py", + "datavisualization/surface_model_numpy/main.py", + "datavisualization/surface_model_numpy/surfacegraph.py", + "datavisualization/surface_numpy/main.py", + "datavisualization/surface_numpy/surfacegraph.py", + "dbus/listnames/listnames.py", + "dbus/pingpong/ping.py", + "dbus/pingpong/pong.py", + "demos/colorpaletteclient/abstractresource.py", + "demos/colorpaletteclient/basiclogin.py", + "demos/colorpaletteclient/main.py", + "demos/colorpaletteclient/paginatedresource.py", + "demos/colorpaletteclient/restservice.py", + "demos/documentviewer/imageviewer/imageviewer.py", + "demos/documentviewer/jsonviewer/jsonviewer.py", + "demos/documentviewer/pdfviewer/pdfviewer.py", + "demos/documentviewer/pdfviewer/zoomselector.py", + "demos/documentviewer/txtviewer/txtviewer.py", + "demos/documentviewer/abstractviewer.py", + "demos/documentviewer/main.py", + "demos/documentviewer/mainwindow.py", + "demos/documentviewer/recentfilemenu.py", + "demos/documentviewer/recentfiles.py", + "demos/documentviewer/viewerfactory.py", + "demos/osmbuildings/main.py", + "demos/osmbuildings/manager.py", + "demos/osmbuildings/request.py", "designer/taskmenuextension/main.py", "designer/taskmenuextension/registertictactoe.py", "designer/taskmenuextension/tictactoeplugin.py", "designer/taskmenuextension/tictactoe.py", "designer/taskmenuextension/tictactoetaskmenu.py", - "external/matplotlib/widget_3dplot.py", + "external/matplotlib/widget_gaussian/widget_gaussian.py", + "external/matplotlib/widget3d/widget3d.py", + "external/networkx/main.py", "external/opencv/webcam_pattern_detection.py", "external/pandas/dataframe_model.py", "external/scikit/staining_colors_separation.py", + "graphs/2d/graphsaudio/main.py", + "graphs/2d/hellographs/main.py", + "graphs/3d/bars/main.py", + "graphs/3d/minimalsurfacegraph/main.py", + "graphs/3d/widgetgraphgallery/bargraph.py", + "graphs/3d/widgetgraphgallery/graphmodifier.py", + "graphs/3d/widgetgraphgallery/highlightseries.py", + "graphs/3d/widgetgraphgallery/main.py", + "graphs/3d/widgetgraphgallery/rainfalldata.py", + "graphs/3d/widgetgraphgallery/scatterdatamodifier.py", + "graphs/3d/widgetgraphgallery/scattergraph.py", + "graphs/3d/widgetgraphgallery/surfacegraph.py", + "graphs/3d/widgetgraphgallery/surfacegraphmodifier.py", + "graphs/3d/widgetgraphgallery/topographicseries.py", + "graphs/3d/widgetgraphgallery/variantbardatamapping.py", + "graphs/3d/widgetgraphgallery/variantbardataproxy.py", + "graphs/3d/widgetgraphgallery/variantdataset.py", + "gui/analogclock/main.py", + "gui/rhiwindow/main.py", + "gui/rhiwindow/rhiwindow.py", + "httpserver/simplehttpserver/main.py", "installer_test/hello.py", - "macextras/macpasteboardmime/macpasteboardmime.py", + "location/mapviewer/main.py", "multimedia/audiooutput/audiooutput.py", + "multimedia/audiosource/audiosource.py", "multimedia/camera/camera.py", + "multimedia/camera/imagesettings.py", + "multimedia/camera/videosettings.py", "multimedia/player/player.py", + "multimedia/screencapture/main.py", + "multimedia/screencapture/screencapturepreview.py", + "multimedia/screencapture/screenlistmodel.py", + "multimedia/screencapture/windowlistmodel.py", "network/blockingfortuneclient/blockingfortuneclient.py", + "network/downloader/downloader.py", "network/fortuneclient/fortuneclient.py", "network/fortuneserver/fortuneserver.py", + "network/googlesuggest/googlesuggest.py", + "network/googlesuggest/main.py", + "network/googlesuggest/searchbox.py", + "network/loopback/dialog.py", + "network/loopback/main.py", "network/threadedfortuneserver/threadedfortuneserver.py", + "networkauth/redditclient/main.py", + "networkauth/redditclient/redditmodel.py", + "networkauth/redditclient/redditwrapper.py", "opengl/contextinfo/contextinfo.py", - "opengl/hellogl2/hellogl2.py", + "opengl/hellogl2/glwidget.py", + "opengl/hellogl2/logo.py", + "opengl/hellogl2/main.py", + "opengl/hellogl2/mainwindow.py", + "opengl/hellogl2/window.py", "opengl/textures/textures.py", + "opengl/threadedopenglwidget/glwidget.py", + "opengl/threadedopenglwidget/main.py", + "opengl/threadedopenglwidget/mainwindow.py", + "opengl/threadedopenglwidget/renderer.py", + "pdf/quickpdfviewer/main.py", + "pdfwidgets/pdfviewer/main.py", + "pdfwidgets/pdfviewer/mainwindow.py", + "pdfwidgets/pdfviewer/zoomselector.py", + "qml/editingmodel/main.py", + "qml/editingmodel/model.py", + "qml/signals/qmltopy1/main.py", + "qml/signals/qmltopy2/main.py", + "qml/signals/qmltopy3/main.py", + "qml/signals/qmltopy4/main.py", + "qml/tutorials/extending-qml/chapter1-basics/basics.py", + "qml/tutorials/extending-qml/chapter2-methods/methods.py", + "qml/tutorials/extending-qml/chapter3-bindings/bindings.py", + "qml/tutorials/extending-qml/chapter4-customPropertyTypes/customPropertyTypes.py", + "qml/tutorials/extending-qml/chapter5-listproperties/listproperties.py", + "qml/tutorials/extending-qml/chapter6-plugins/Charts/piechart.py", + "qml/tutorials/extending-qml/chapter6-plugins/Charts/pieslice.py", + "qml/textproperties/main.py", + "qml/usingmodel/usingmodel.py", + "quick/models/objectlistmodel/objectlistmodel.py", + "quick/models/stringlistmodel/stringlistmodel.py", + "quick/painteditem/painteditem.py", + "quick/rendercontrol/rendercontrol_opengl/cuberenderer.py", + "quick/rendercontrol/rendercontrol_opengl/main.py", + "quick/rendercontrol/rendercontrol_opengl/window_singlethreaded.py", + "quick/scenegraph/openglunderqml/main.py", + "quick/scenegraph/openglunderqml/squircle.py", + "quick/scenegraph/openglunderqml/squirclerenderer.py", + "quick/scenegraph/scenegraph_customgeometry/main.py", + "quick/scenegraph/window/main.py", + "quick3d/customgeometry/examplepoint.py", + "quick3d/customgeometry/exampletriangle.py", + "quick3d/customgeometry/main.py", + "quick3d/intro/main.py", + "quick3d/proceduraltexture/gradienttexture.py", + "quick3d/proceduraltexture/main.py", + "quickcontrols/contactslist/main.py", "quickcontrols/gallery/gallery.py", - "quickcontrols/filesystemexplorer/filesystemexplorer.py", + "quickcontrols/filesystemexplorer/main.py", "quick/painteditem/painteditem.py", "remoteobjects/modelview/modelviewclient.py", "remoteobjects/modelview/modelviewserver.py", "samplebinding/main.py", + "serialbus/can/bitratebox.py", + "serialbus/can/canbusdeviceinfo.py", + "serialbus/can/canbusdeviceinfodialog.py", + "serialbus/can/connectdialog.py", + "serialbus/can/main.py", + "serialbus/can/mainwindow.py", + "serialbus/can/receivedframesmodel.py", + "serialbus/can/receivedframesview.py", + "serialbus/can/sendframebox.py", + "serialbus/modbus/modbusclient/main.py", + "serialbus/modbus/modbusclient/mainwindow.py", + "serialbus/modbus/modbusclient/settingsdialog.py", + "serialbus/modbus/modbusclient/writeregistermodel.py", + "serialport/terminal/console.py", + "serialport/terminal/main.py", + "serialport/terminal/mainwindow.py", + "serialport/terminal/settingsdialog.py", + "spatialaudio/audiospanning/main.py", + "speech/hello_speak/main.py", + "speech/hello_speak/mainwindow.py", "sql/books/bookdelegate.py", "sql/books/bookwindow.py", "sql/books/createdb.py", "sql/books/main.py", - "texttospeech/hello_speak/hello_speak.py", + "sql/relationaltablemodel/connection.py", + "sql/relationaltablemodel/relationaltablemodel.py", + "statemachine/moveblocks/moveblocks.py", + "statemachine/ping_pong/ping_pong.py", + "statemachine/rogue/rogue.py", + "statemachine/trafficlight/trafficlight.py", + "tutorials/finance_manager/part1/financemodel.py", + "tutorials/finance_manager/part1/main.py", + "tutorials/finance_manager/part2/database.py", + "tutorials/finance_manager/part2/financemodel.py", + "tutorials/finance_manager/part2/main.py", + "tutorials/finance_manager/part3/Backend/database.py", + "tutorials/finance_manager/part3/Backend/main.py", + "tutorials/finance_manager/part3/Backend/rest_api.py", + "tutorials/finance_manager/part3/Frontend/financemodel.py", + "tutorials/finance_manager/part3/Frontend/main.py", "uitools/uiloader/uiloader.py", + "utils/pyside_config.py", "webchannel/standalone/core.py", "webchannel/standalone/dialog.py", "webchannel/standalone/main.py", "webchannel/standalone/websocketclientwrapper.py", "webchannel/standalone/websockettransport.py", "webenginequick/nanobrowser/quicknanobrowser.py", - "webenginewidgets/simplebrowser/simplebrowser.py", - "webenginewidgets/tabbedbrowser/bookmarkwidget.py", - "webenginewidgets/tabbedbrowser/browsertabwidget.py", - "webenginewidgets/tabbedbrowser/downloadwidget.py", - "webenginewidgets/tabbedbrowser/findtoolbar.py", - "webenginewidgets/tabbedbrowser/historywindow.py", - "webenginewidgets/tabbedbrowser/main.py", - "webenginewidgets/tabbedbrowser/webengineview.py", + "webenginewidgets/markdowneditor/document.py", + "webenginewidgets/markdowneditor/main.py", + "webenginewidgets/markdowneditor/mainwindow.py", + "webenginewidgets/markdowneditor/previewpage.py", + "webenginewidgets/notifications/main.py", + "webenginewidgets/notifications/notificationpopup.py", + "webenginewidgets/simplebrowser/browser.py", + "webenginewidgets/simplebrowser/browserwindow.py", + "webenginewidgets/simplebrowser/downloadmanagerwidget.py", + "webenginewidgets/simplebrowser/downloadwidget.py", + "webenginewidgets/simplebrowser/main.py", + "webenginewidgets/simplebrowser/tabwidget.py", + "webenginewidgets/simplebrowser/webauthdialog.py", + "webenginewidgets/simplebrowser/webpage.py", + "webenginewidgets/simplebrowser/webpopupwindow.py", + "webenginewidgets/simplebrowser/webview.py", + "webenginewidgets/widgetsnanobrowser/widgetsnanobrowser.py", + "webview/minibrowser/main.py", "widgetbinding/dialog.py", "widgetbinding/main.py", "widgetbinding/registerwigglywidget.py", @@ -88,17 +286,27 @@ "widgets/animation/appchooser/appchooser.py", "widgets/animation/easing/easing.py", "widgets/animation/states/states.py", - "widgets/codeeditor/codeeditor.py", - "widgets/codeeditor/main.py", + "widgets/desktop/screenshot/screenshot.py", + "widgets/desktop/systray/main.py", + "widgets/desktop/systray/window.py", "widgets/dialogs/classwizard/classwizard.py", + "widgets/dialogs/classwizard/listchooser.py", "widgets/dialogs/extension/extension.py", - "widgets/dialogs/findfiles/findfiles.py", + "widgets/dialogs/licensewizard/licensewizard.py", + "widgets/dialogs/licensewizard/main.py", "widgets/dialogs/standarddialogs/standarddialogs.py", + "widgets/dialogs/tabdialog/tabdialog.py", "widgets/dialogs/trivialwizard/trivialwizard.py", + "widgets/draganddrop/draggableicons/draggableicons.py", "widgets/draganddrop/draggabletext/draggabletext.py", - "widgets/effects/lighting.py", - "widgets/gallery/main.py", - "widgets/gallery/widgetgallery.py", + "widgets/draganddrop/dropsite/droparea.py", + "widgets/draganddrop/dropsite/dropsitewindow.py", + "widgets/draganddrop/dropsite/main.py", + "widgets/effects/blurpicker/blureffect.py", + "widgets/effects/blurpicker/blurpicker.py", + "widgets/effects/blurpicker/main.py", + "widgets/effects/lighting/lighting.py", + "widgets/gettext/main.py", "widgets/graphicsview/anchorlayout/anchorlayout.py", "widgets/graphicsview/collidingmice/collidingmice.py", "widgets/graphicsview/diagramscene/diagramscene.py", @@ -106,44 +314,49 @@ "widgets/graphicsview/elasticnodes/elasticnodes.py", "widgets/imageviewer/imageviewer.py", "widgets/imageviewer/main.py", - "widgets/itemviews/addressbook/adddialogwidget.py", - "widgets/itemviews/addressbook/addressbook.py", - "widgets/itemviews/addressbook/addresswidget.py", - "widgets/itemviews/addressbook/newaddresstab.py", - "widgets/itemviews/addressbook/tablemodel.py", + "widgets/itemviews/address_book/adddialogwidget.py", + "widgets/itemviews/address_book/address_book.py", + "widgets/itemviews/address_book/addresswidget.py", + "widgets/itemviews/address_book/newaddresstab.py", + "widgets/itemviews/address_book/tablemodel.py", "widgets/itemviews/basicfiltermodel/basicsortfiltermodel.py", + "widgets/itemviews/dirview/dirview.py", + "widgets/itemviews/editabletreemodel/main.py", + "widgets/itemviews/editabletreemodel/mainwindow.py", + "widgets/itemviews/editabletreemodel/treeitem.py", + "widgets/itemviews/editabletreemodel/treemodel.py", "widgets/itemviews/fetchmore/fetchmore.py", + "widgets/itemviews/jsonmodel/jsonmodel.py", + "widgets/itemviews/spinboxdelegate/spinboxdelegate.py", + "widgets/itemviews/spreadsheet/main.py", + "widgets/itemviews/spreadsheet/spreadsheet.py", + "widgets/itemviews/spreadsheet/spreadsheetdelegate.py", + "widgets/itemviews/spreadsheet/spreadsheetitem.py", "widgets/itemviews/stardelegate/stardelegate.py", "widgets/itemviews/stardelegate/stareditor.py", "widgets/itemviews/stardelegate/starrating.py", "widgets/layouts/basiclayouts/basiclayouts.py", + "widgets/layouts/borderlayout/borderlayout.py", "widgets/layouts/dynamiclayouts/dynamiclayouts.py", "widgets/layouts/flowlayout/flowlayout.py", + "widgets/linguist/main.py", "widgets/mainwindows/application/application.py", "widgets/mainwindows/dockwidgets/dockwidgets.py", "widgets/mainwindows/mdi/mdi.py", "widgets/painting/basicdrawing/basicdrawing.py", "widgets/painting/concentriccircles/concentriccircles.py", + "widgets/painting/painter/painter.py", + "widgets/painting/plot/plot.py", + "widgets/rhi/simplerhiwidget/examplewidget.py", + "widgets/rhi/simplerhiwidget/main.py", "widgets/richtext/orderform/orderform.py", "widgets/richtext/syntaxhighlighter/syntaxhighlighter.py", + "widgets/richtext/textedit/main.py", + "widgets/richtext/textedit/textedit.py", "widgets/richtext/textobject/textobject.py", - "widgets/state-machine/eventtrans/eventtrans.py", - "widgets/state-machine/factstates/factstates.py", - "widgets/state-machine/pingpong/pingpong.py", - "widgets/state-machine/rogue/rogue.py", - "widgets/state-machine/trafficlight/trafficlight.py", - "widgets/state-machine/twowaybutton/twowaybutton.py", - "widgets/systray/main.py", - "widgets/systray/window.py", - "widgets/tetrix/tetrix.py", "widgets/threads/thread_signals.py", - "widgets/tutorials/addressbook/part1.py", - "widgets/tutorials/addressbook/part2.py", - "widgets/tutorials/addressbook/part3.py", - "widgets/tutorials/addressbook/part4.py", - "widgets/tutorials/addressbook/part5.py", - "widgets/tutorials/addressbook/part6.py", - "widgets/tutorials/addressbook/part7.py", + "widgets/tools/regularexpression/regularexpression.py", + "widgets/tools/regularexpression/regularexpressiondialog.py", "widgets/tutorials/cannon/t10.py", "widgets/tutorials/cannon/t11.py", "widgets/tutorials/cannon/t12.py", @@ -158,5 +371,20 @@ "widgets/tutorials/cannon/t7.py", "widgets/tutorials/cannon/t8.py", "widgets/tutorials/cannon/t9.py", + "widgets/tutorials/modelview/1_readonly.py", + "widgets/tutorials/modelview/2_formatting.py", + "widgets/tutorials/modelview/3_changingmodel.py", + "widgets/tutorials/modelview/4_headers.py", + "widgets/tutorials/modelview/5_edit.py", + "widgets/tutorials/modelview/6_treeview.py", + "widgets/tutorials/modelview/7_selections.py", + "widgets/widgets/charactermap/characterwidget.py", + "widgets/widgets/charactermap/fontinfodialog.py", + "widgets/widgets/charactermap/main.py", + "widgets/widgets/charactermap/mainwindow.py", + "widgets/widgets/digitalclock/digitalclock.py", + "widgets/widgets/tetrix/tetrix.py", + "widgets/widgetsgallery/main.py", + "widgets/widgetsgallery/widgetgallery.py", "xml/dombookmarks/dombookmarks.py"] } diff --git a/examples/graphs/2d/hellographs/HelloGraphs/Main.qml b/examples/graphs/2d/hellographs/HelloGraphs/Main.qml index 815e365e..268bf99e 100644 --- a/examples/graphs/2d/hellographs/HelloGraphs/Main.qml +++ b/examples/graphs/2d/hellographs/HelloGraphs/Main.qml @@ -42,7 +42,7 @@ Item { subTickCount: 9 } theme: GraphsTheme { - colorScheme: Qt.Dark + colorScheme: GraphsTheme.ColorScheme.Dark theme: GraphsTheme.Theme.QtGreen } //! [bargraph] @@ -77,7 +77,7 @@ Item { readonly property color c1: "#DBEB00" readonly property color c2: "#373F26" readonly property color c3: Qt.lighter(c2, 1.5) - colorScheme: Qt.Dark + colorScheme: GraphsTheme.ColorScheme.Dark seriesColors: ["#2CDE85", "#DBEB00"] grid.mainColor: c3 grid.subColor: c2 diff --git a/examples/graphs/3d/widgetgraphgallery/bargraph.py b/examples/graphs/3d/widgetgraphgallery/bargraph.py index 3259e8a3..3e3cf109 100644 --- a/examples/graphs/3d/widgetgraphgallery/bargraph.py +++ b/examples/graphs/3d/widgetgraphgallery/bargraph.py @@ -180,9 +180,9 @@ class BarGraph(QObject): modeGroup = QButtonGroup(self._barsWidget) modeWeather = QRadioButton("Temperature Data", self._barsWidget) modeWeather.setChecked(True) - modeCustomProxy = QRadioButton("Custom Proxy Data", self._barsWidget) + modelProxy = QRadioButton("Model Proxy Data", self._barsWidget) modeGroup.addButton(modeWeather) - modeGroup.addButton(modeCustomProxy) + modeGroup.addButton(modelProxy) vLayout.addWidget(QLabel("Rotate horizontally")) vLayout.addWidget(rotationSliderX, 0, Qt.AlignmentFlag.AlignTop) @@ -215,7 +215,7 @@ class BarGraph(QObject): vLayout.addWidget(QLabel("Axis label rotation")) vLayout.addWidget(axisLabelRotationSlider, 0, Qt.AlignmentFlag.AlignTop) vLayout.addWidget(modeWeather, 0, Qt.AlignmentFlag.AlignTop) - vLayout.addWidget(modeCustomProxy, 1, Qt.AlignmentFlag.AlignTop) + vLayout.addWidget(modelProxy, 1, Qt.AlignmentFlag.AlignTop) modifier = GraphModifier(barsGraph, self) modifier.changeTheme(themeList.currentIndex()) @@ -260,7 +260,7 @@ class BarGraph(QObject): axisLabelRotationSlider.valueChanged.connect(modifier.changeLabelRotation) modeWeather.toggled.connect(modifier.setDataModeToWeather) - modeCustomProxy.toggled.connect(modifier.setDataModeToCustom) + modelProxy.toggled.connect(modifier.setDataModeToModel) modeWeather.toggled.connect(seriesCheckBox.setEnabled) modeWeather.toggled.connect(rangeList.setEnabled) modeWeather.toggled.connect(axisTitlesVisibleCB.setEnabled) diff --git a/examples/graphs/3d/widgetgraphgallery/graphmodifier.py b/examples/graphs/3d/widgetgraphgallery/graphmodifier.py index 5c4d23c9..b77d0deb 100644 --- a/examples/graphs/3d/widgetgraphgallery/graphmodifier.py +++ b/examples/graphs/3d/widgetgraphgallery/graphmodifier.py @@ -336,7 +336,7 @@ class GraphModifier(QObject): self.changeDataMode(False) @Slot(bool) - def setDataModeToCustom(self, enabled): + def setDataModeToModel(self, enabled): if enabled: self.changeDataMode(True) diff --git a/examples/graphs/3d/widgetgraphgallery/highlightseries.py b/examples/graphs/3d/widgetgraphgallery/highlightseries.py index 58a0d531..be785212 100644 --- a/examples/graphs/3d/widgetgraphgallery/highlightseries.py +++ b/examples/graphs/3d/widgetgraphgallery/highlightseries.py @@ -23,7 +23,7 @@ class HighlightSeries(QSurface3DSeries): self._height = 100 self._srcWidth = 0 self._srcHeight = 0 - self._position = {} + self._position = QPoint() self._topographicSeries = None self._minHeight = 0.0 self._height_adjustment = 5.0 @@ -52,16 +52,16 @@ class HighlightSeries(QSurface3DSeries): halfWidth = self._width / 2 halfHeight = self._height / 2 - startX = position.y() - halfWidth + startX = position.x() - halfWidth if startX < 0: startX = 0 - endX = position.y() + halfWidth + endX = position.x() + halfWidth if endX > (self._srcWidth - 1): endX = self._srcWidth - 1 - startZ = position.x() - halfHeight + startZ = position.y() - halfHeight if startZ < 0: startZ = 0 - endZ = position.x() + halfHeight + endZ = position.y() + halfHeight if endZ > (self._srcHeight - 1): endZ = self._srcHeight - 1 @@ -71,10 +71,10 @@ class HighlightSeries(QSurface3DSeries): for i in range(int(startZ), int(endZ)): newRow = [] srcRow = srcArray[i] - for j in range(startX, endX): - pos = srcRow.at(j).position() + for j in range(int(startX), int(endX)): + pos = QVector3D(srcRow[j].position()) pos.setY(pos.y() + self._height_adjustment) - item = QSurfaceDataItem(QVector3D(pos)) + item = QSurfaceDataItem(pos) newRow.append(item) dataArray.append(newRow) self.dataProxy().resetArray(dataArray) diff --git a/examples/graphs/3d/widgetgraphgallery/rainfalldata.py b/examples/graphs/3d/widgetgraphgallery/rainfalldata.py index a5339672..9fe95aff 100644 --- a/examples/graphs/3d/widgetgraphgallery/rainfalldata.py +++ b/examples/graphs/3d/widgetgraphgallery/rainfalldata.py @@ -6,12 +6,8 @@ import sys from pathlib import Path -from PySide6.QtCore import QFile, QIODevice, QObject -from PySide6.QtGraphs import (QBar3DSeries, QCategory3DAxis, QValue3DAxis) - -from variantbardataproxy import VariantBarDataProxy -from variantbardatamapping import VariantBarDataMapping -from variantdataset import VariantDataSet +from PySide6.QtCore import QFile, QIODevice, QObject, QRangeModel +from PySide6.QtGraphs import (QBar3DSeries, QCategory3DAxis, QValue3DAxis, QItemModelBarDataProxy) MONTHS = ["January", "February", "March", "April", @@ -19,6 +15,40 @@ MONTHS = ["January", "February", "March", "April", "November", "December"] +def read_data(file_path): + """Return a tuple of data matrix/first year.""" + dataFile = QFile(file_path) + if not dataFile.open(QIODevice.OpenModeFlag.ReadOnly | QIODevice.OpenModeFlag.Text): + print("Unable to open data file:", dataFile.fileName(), file=sys.stderr) + return None, None + + last_year = -1 + first_year = -1 + result = [] + data = dataFile.readAll().data().decode("utf8") + for line in data.split("\n"): + if line and not line.startswith("#"): # Ignore comments + tokens = line.split(",") + # Each line has three data items: Year, month, and + # rainfall value + if len(tokens) >= 3: + # Store year and month as strings, and rainfall value + # as double into a variant data item and add the item to + # the item list. + year = int(tokens[0].strip()) + month = int(tokens[1].strip()) + value = float(tokens[2].strip()) + if year != last_year: + if first_year == -1: + first_year = last_year + result.append([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + last_year = year + yearly_values = result[len(result) - 1] + yearly_values[month - 1] = value + + return result, first_year + + class RainfallData(QObject): def __init__(self): @@ -27,9 +57,7 @@ class RainfallData(QObject): self._rowCount = 0 self._years = [] self._numericMonths = [] - self._proxy = VariantBarDataProxy() self._mapping = None - self._dataSet = None self._series = QBar3DSeries() self._valueAxis = QValue3DAxis() self._rowAxis = QCategory3DAxis() @@ -41,10 +69,13 @@ class RainfallData(QObject): self._columnCount = len(self._numericMonths) - self.updateYearsList(2010, 2022) - - # Create proxy and series - self._proxy = VariantBarDataProxy() + file_path = Path(__file__).resolve().parent / "data" / "raindata.txt" + values, first_year = read_data(file_path) + assert (values) + self.updateYearsList(first_year, first_year + len(values)) + self._model = QRangeModel(values, self) + self._proxy = QItemModelBarDataProxy(self._model) + self._proxy.setUseModelCategories(True) self._series = QBar3DSeries(self._proxy) self._series.setItemLabelFormat("%.1f mm") @@ -68,8 +99,6 @@ class RainfallData(QObject): self._colAxis.setTitleVisible(True) self._valueAxis.setTitleVisible(True) - self.addDataSet() - def customSeries(self): return self._series @@ -87,40 +116,3 @@ class RainfallData(QObject): for i in range(start, end + 1): self._years.append(str(i)) self._rowCount = len(self._years) - - def addDataSet(self): - # Create a new variant data set and data item list - self._dataSet = VariantDataSet() - itemList = [] - - # Read data from a data file into the data item list - file_path = Path(__file__).resolve().parent / "data" / "raindata.txt" - dataFile = QFile(file_path) - if dataFile.open(QIODevice.OpenModeFlag.ReadOnly | QIODevice.OpenModeFlag.Text): - data = dataFile.readAll().data().decode("utf8") - for line in data.split("\n"): - if line and not line.startswith("#"): # Ignore comments - tokens = line.split(",") - # Each line has three data items: Year, month, and - # rainfall value - if len(tokens) >= 3: - # Store year and month as strings, and rainfall value - # as double into a variant data item and add the item to - # the item list. - newItem = [] - newItem.append(tokens[0].strip()) - newItem.append(tokens[1].strip()) - newItem.append(float(tokens[2].strip())) - itemList.append(newItem) - else: - print("Unable to open data file:", dataFile.fileName(), - file=sys.stderr) - - # Add items to the data set and set it to the proxy - self._dataSet.addItems(itemList) - self._proxy.setDataSet(self._dataSet) - - # Create new mapping for the data and set it to the proxy - self._mapping = VariantBarDataMapping(0, 1, 2, - self._years, self._numericMonths) - self._proxy.setMapping(self._mapping) diff --git a/examples/graphs/3d/widgetgraphgallery/variantbardatamapping.py b/examples/graphs/3d/widgetgraphgallery/variantbardatamapping.py deleted file mode 100644 index 5b1986b8..00000000 --- a/examples/graphs/3d/widgetgraphgallery/variantbardatamapping.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (C) 2023 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from __future__ import annotations - -from PySide6.QtCore import QObject, Signal - - -class VariantBarDataMapping(QObject): - - rowIndexChanged = Signal() - columnIndexChanged = Signal() - valueIndexChanged = Signal() - rowCategoriesChanged = Signal() - columnCategoriesChanged = Signal() - mappingChanged = Signal() - - def __init__(self, rowIndex, columnIndex, valueIndex, - rowCategories=[], columnCategories=[]): - super().__init__(None) - self._rowIndex = rowIndex - self._columnIndex = columnIndex - self._valueIndex = valueIndex - self._rowCategories = rowCategories - self._columnCategories = columnCategories - - def setRowIndex(self, index): - self._rowIndex = index - self.mappingChanged.emit() - - def rowIndex(self): - return self._rowIndex - - def setColumnIndex(self, index): - self._columnIndex = index - self.mappingChanged.emit() - - def columnIndex(self): - return self._columnIndex - - def setValueIndex(self, index): - self._valueIndex = index - self.mappingChanged.emit() - - def valueIndex(self): - return self._valueIndex - - def setRowCategories(self, categories): - self._rowCategories = categories - self.mappingChanged.emit() - - def rowCategories(self): - return self._rowCategories - - def setColumnCategories(self, categories): - self._columnCategories = categories - self.mappingChanged.emit() - - def columnCategories(self): - return self._columnCategories - - def remap(self, rowIndex, columnIndex, valueIndex, - rowCategories=[], columnCategories=[]): - self._rowIndex = rowIndex - self._columnIndex = columnIndex - self._valueIndex = valueIndex - self._rowCategories = rowCategories - self._columnCategories = columnCategories - self.mappingChanged.emit() diff --git a/examples/graphs/3d/widgetgraphgallery/variantbardataproxy.py b/examples/graphs/3d/widgetgraphgallery/variantbardataproxy.py deleted file mode 100644 index 9cd71b1a..00000000 --- a/examples/graphs/3d/widgetgraphgallery/variantbardataproxy.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright (C) 2023 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from __future__ import annotations - -from PySide6.QtCore import Slot -from PySide6.QtGraphs import QBarDataProxy, QBarDataItem - - -class VariantBarDataProxy(QBarDataProxy): - - def __init__(self): - super().__init__() - self._dataSet = None - self._mapping = None - - def setDataSet(self, newSet): - if self._dataSet: - self._dataSet.itemsAdded.disconnect(self.handleItemsAdded) - self._dataSet.dataCleared.disconnect(self.handleDataCleared) - - self._dataSet = newSet - - if self._dataSet: - self._dataSet.itemsAdded.connect(self.handleItemsAdded) - self._dataSet.dataCleared.connect(self.handleDataCleared) - self.resolveDataSet() - - def dataSet(self): - return self._dataSet.data() - - # Map key (row, column, value) to value index in data item (VariantItem). - # Doesn't gain ownership of mapping, but does connect to it to listen for - # mapping changes. Modifying mapping that is set to proxy will trigger - # dataset re-resolving. - def setMapping(self, mapping): - if self._mapping: - self._mapping.mappingChanged.disconnect(self.handleMappingChanged) - - self._mapping = mapping - - if self._mapping: - self._mapping.mappingChanged.connect(self.handleMappingChanged) - - self.resolveDataSet() - - def mapping(self): - return self._mapping.data() - - @Slot(int, int) - def handleItemsAdded(self, index, count): - # Resolve new items - self.resolveDataSet() - - @Slot() - def handleDataCleared(self): - # Data cleared, reset array - self.resetArray(None) - - @Slot() - def handleMappingChanged(self): - self.resolveDataSet() - - # Resolve entire dataset into QBarDataArray. - def resolveDataSet(self): - # If we have no data or mapping, or the categories are not defined, - # simply clear the array - if (not self._dataSet or not self._mapping - or not self._mapping.rowCategories() - or not self._mapping.columnCategories()): - self.resetArray() - return - - itemList = self._dataSet.itemList() - - rowIndex = self._mapping.rowIndex() - columnIndex = self._mapping.columnIndex() - valueIndex = self._mapping.valueIndex() - rowList = self._mapping.rowCategories() - columnList = self._mapping.columnCategories() - - # Sort values into rows and columns - itemValueMap = {} - for item in itemList: - key = str(item[rowIndex]) - v = itemValueMap.get(key) - if not v: - v = {} - itemValueMap[key] = v - v[str(item[columnIndex])] = float(item[valueIndex]) - - # Create a new data array in format the parent class understands - newProxyArray = [] - for rowKey in rowList: - newProxyRow = [] - for i in range(0, len(columnList)): - item = QBarDataItem(itemValueMap[rowKey][columnList[i]]) - newProxyRow.append(item) - newProxyArray.append(newProxyRow) - - # Finally, reset the data array in the parent class - self.resetArray(newProxyArray) diff --git a/examples/graphs/3d/widgetgraphgallery/variantdataset.py b/examples/graphs/3d/widgetgraphgallery/variantdataset.py deleted file mode 100644 index c9b8ab1a..00000000 --- a/examples/graphs/3d/widgetgraphgallery/variantdataset.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (C) 2023 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from __future__ import annotations - -from PySide6.QtCore import QObject, Signal - - -class VariantDataSet(QObject): - - itemsAdded = Signal(int, int) - dataCleared = Signal() - - def __init__(self): - super().__init__() - self._variantData = [] - - def clear(self): - for item in self._variantData: - item.clear() - del item - - self._variantData.clear() - self.dataCleared.emit() - - def addItem(self, item): - self._variantData.append(item) - addIndex = len(self._variantData) - - self.itemsAdded.emit(addIndex, 1) - return addIndex - - def addItems(self, itemList): - newCount = len(itemList) - addIndex = len(self._variantData) - self._variantData.extend(itemList) - self.itemsAdded.emit(addIndex, newCount) - return addIndex - - def itemList(self): - return self._variantData diff --git a/examples/graphs/3d/widgetgraphgallery/widgetgraphgallery.pyproject b/examples/graphs/3d/widgetgraphgallery/widgetgraphgallery.pyproject index ebc680f6..4e4a691b 100644 --- a/examples/graphs/3d/widgetgraphgallery/widgetgraphgallery.pyproject +++ b/examples/graphs/3d/widgetgraphgallery/widgetgraphgallery.pyproject @@ -9,9 +9,6 @@ "surfacegraph.py", "surfacegraphmodifier.py", "topographicseries.py", - "variantbardatamapping.py", - "variantbardataproxy.py", - "variantdataset.py", "data/layer_1.png", "data/layer_2.png", "data/layer_3.png", diff --git a/examples/gui/rhiwindow/rhiwindow.py b/examples/gui/rhiwindow/rhiwindow.py index d4522e51..e9b6c1d1 100644 --- a/examples/gui/rhiwindow/rhiwindow.py +++ b/examples/gui/rhiwindow/rhiwindow.py @@ -176,7 +176,7 @@ class RhiWindow(QWindow): self.m_rhi = QRhi.create(QRhi.Implementation.D3D12, params) elif self.m_graphicsApi == QRhi.Implementation.Metal: params = QRhiMetalInitParams() - self.m_rhi.reset(QRhi.create(QRhi.Implementation.Metal, params)) + self.m_rhi = QRhi.create(QRhi.Implementation.Metal, params) if not self.m_rhi: qFatal("Failed to create RHI backend") diff --git a/examples/location/mapviewer/MapViewer/Main.qml b/examples/location/mapviewer/MapViewer/Main.qml index f4ae7ea0..6fcf37db 100644 --- a/examples/location/mapviewer/MapViewer/Main.qml +++ b/examples/location/mapviewer/MapViewer/Main.qml @@ -1,6 +1,7 @@ // Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +import QtCore import QtQuick import QtQuick.Controls import QtLocation @@ -221,7 +222,10 @@ ApplicationWindow { stackView.pop(page) switch (state) { case "FollowMe": - mapview.followme = !mapview.followme + if (!mapview.followme && (permission.status !== Qt.Granted)) + permissionDialog.open(); + else + mapview.followme = !mapview.followme break case "MiniMap": toggleMiniMapState() @@ -457,4 +461,42 @@ support" } } } + + LocationPermission { + id: permission + accuracy: LocationPermission.Precise + availability: LocationPermission.WhenInUse + } + + Dialog { + id: permissionDialog + anchors.centerIn: parent + padding: 20 + standardButtons: (permission.status === Qt.Denied) ? Dialog.Close + : Dialog.Close | Dialog.Ok + closePolicy: Dialog.NoAutoClose + title: qsTr("Permission") + + Label { + id: permissionRequestText + text: (permission.status === Qt.Denied) + ? qsTr("Grant the location permission then open the app again.") + : qsTr("Location permission is needed.") + } + + onAccepted: { + if (permission.status !== Qt.Denied) + permission.request(); + } + + onStandardButtonsChanged: { + if (standardButtons & Dialog.Ok) + standardButton(Dialog.Ok).text = qsTr("Request Permission"); + } + + Component.onCompleted: { + if (permission.status !== Qt.Granted) + open(); + } + } } diff --git a/examples/location/mapviewer/MapViewer/forms/MessageForm.ui.qml b/examples/location/mapviewer/MapViewer/forms/MessageForm.ui.qml index f2206111..426c7275 100644 --- a/examples/location/mapviewer/MapViewer/forms/MessageForm.ui.qml +++ b/examples/location/mapviewer/MapViewer/forms/MessageForm.ui.qml @@ -66,3 +66,4 @@ Item { } } } + diff --git a/examples/location/mapviewer/main.py b/examples/location/mapviewer/main.py index 1eefb497..c7d9da4c 100644 --- a/examples/location/mapviewer/main.py +++ b/examples/location/mapviewer/main.py @@ -47,6 +47,7 @@ if __name__ == "__main__": application = QGuiApplication(sys.argv) name = "QtLocation Mapviewer example" QCoreApplication.setApplicationName(name) + QGuiApplication.setDesktopFileName(QCoreApplication.applicationName()) args = sys.argv[1:] if "--help" in args: diff --git a/examples/multimedia/audiooutput/audiooutput.py b/examples/multimedia/audiooutput/audiooutput.py index b0ab567d..cadfe2d0 100644 --- a/examples/multimedia/audiooutput/audiooutput.py +++ b/examples/multimedia/audiooutput/audiooutput.py @@ -11,8 +11,7 @@ from struct import pack from PySide6.QtCore import (QByteArray, QIODevice, Qt, QSysInfo, QTimer, qWarning, Slot) -from PySide6.QtMultimedia import (QAudio, QAudioFormat, - QAudioSink, QMediaDevices) +from PySide6.QtMultimedia import (QAudioFormat, QAudioSink, QMediaDevices, QtAudio) from PySide6.QtWidgets import (QApplication, QComboBox, QHBoxLayout, QLabel, QMainWindow, QPushButton, QSlider, QVBoxLayout, QWidget) @@ -211,7 +210,7 @@ class AudioTest(QMainWindow): @Slot() def pull_timer_expired(self): - if self.m_audioSink is not None and self.m_audioSink.state() != QAudio.State.StoppedState: + if self.m_audioSink is not None and self.m_audioSink.state() != QtAudio.State.StoppedState: bytes_free = self.m_audioSink.bytesFree() data = self.m_generator.read(bytes_free) if data: @@ -236,28 +235,28 @@ class AudioTest(QMainWindow): @Slot() def toggle_suspend_resume(self): - if self.m_audioSink.state() == QAudio.State.SuspendedState: + if self.m_audioSink.state() == QtAudio.State.SuspendedState: qWarning("status: Suspended, resume()") self.m_audioSink.resume() self.m_suspendResumeButton.setText(self.SUSPEND_LABEL) - elif self.m_audioSink.state() == QAudio.State.ActiveState: + elif self.m_audioSink.state() == QtAudio.State.ActiveState: qWarning("status: Active, suspend()") self.m_audioSink.suspend() self.m_suspendResumeButton.setText(self.RESUME_LABEL) - elif self.m_audioSink.state() == QAudio.State.StoppedState: + elif self.m_audioSink.state() == QtAudio.State.StoppedState: qWarning("status: Stopped, resume()") self.m_audioSink.resume() self.m_suspendResumeButton.setText(self.SUSPEND_LABEL) - elif self.m_audioSink.state() == QAudio.State.IdleState: + elif self.m_audioSink.state() == QtAudio.State.IdleState: qWarning("status: IdleState") state_map = { - QAudio.State.ActiveState: "ActiveState", - QAudio.State.SuspendedState: "SuspendedState", - QAudio.State.StoppedState: "StoppedState", - QAudio.State.IdleState: "IdleState"} + QtAudio.State.ActiveState: "ActiveState", + QtAudio.State.SuspendedState: "SuspendedState", + QtAudio.State.StoppedState: "StoppedState", + QtAudio.State.IdleState: "IdleState"} - @Slot("QAudio::State") + @Slot("QtAudio::State") def handle_state_changed(self, state): state = self.state_map.get(state, 'Unknown') qWarning(f"state = {state}") diff --git a/examples/multimedia/audiosource/audiosource.py b/examples/multimedia/audiosource/audiosource.py index 1c0d9841..807a7052 100644 --- a/examples/multimedia/audiosource/audiosource.py +++ b/examples/multimedia/audiosource/audiosource.py @@ -19,7 +19,7 @@ import sys import PySide6 from PySide6.QtCore import QByteArray, QMargins, Qt, Slot, qWarning from PySide6.QtGui import QPainter, QPalette -from PySide6.QtMultimedia import QAudio, QAudioDevice, QAudioFormat, QAudioSource, QMediaDevices +from PySide6.QtMultimedia import QAudioDevice, QAudioFormat, QAudioSource, QMediaDevices, QtAudio from PySide6.QtWidgets import (QApplication, QComboBox, QPushButton, QSlider, QVBoxLayout, QWidget, QLabel) @@ -163,10 +163,10 @@ class InputTest(QWidget): self.m_audio_info = AudioInfo(format) self.m_audio_input = QAudioSource(device_info, format) - initial_volume = QAudio.convertVolume( + initial_volume = QtAudio.convertVolume( self.m_audio_input.volume(), - QAudio.VolumeScale.LinearVolumeScale, - QAudio.VolumeScale.LogarithmicVolumeScale, + QtAudio.VolumeScale.LinearVolumeScale, + QtAudio.VolumeScale.LogarithmicVolumeScale, ) self.m_volume_slider.setValue(int(round(initial_volume * 100))) self.toggle_mode() @@ -195,10 +195,10 @@ class InputTest(QWidget): def toggle_suspend(self): # toggle suspend/resume state = self.m_audio_input.state() - if (state == QAudio.State.SuspendedState) or (state == QAudio.State.StoppedState): + if (state == QtAudio.State.SuspendedState) or (state == QtAudio.State.StoppedState): self.m_audio_input.resume() self.m_suspend_resume_button.setText("Suspend recording") - elif state == QAudio.State.ActiveState: + elif state == QtAudio.State.ActiveState: self.m_audio_input.suspend() self.m_suspend_resume_button.setText("Resume recording") # else no-op @@ -211,9 +211,9 @@ class InputTest(QWidget): @Slot(int) def slider_changed(self, value): - linearVolume = QAudio.convertVolume(value / float(100), - QAudio.VolumeScale.LogarithmicVolumeScale, - QAudio.VolumeScale.LinearVolumeScale) + linearVolume = QtAudio.convertVolume(value / float(100), + QtAudio.VolumeScale.LogarithmicVolumeScale, + QtAudio.VolumeScale.LinearVolumeScale) self.m_audio_input.setVolume(linearVolume) diff --git a/examples/multimedia/camera/camera.py b/examples/multimedia/camera/camera.py index 90a8fed4..12971b4d 100644 --- a/examples/multimedia/camera/camera.py +++ b/examples/multimedia/camera/camera.py @@ -111,7 +111,6 @@ class Camera(QMainWindow): self._ui.captureWidget.currentChanged.connect(self.updateCaptureMode) self._ui.metaDataButton.clicked.connect(self.showMetaDataDialog) - self._ui.exposureCompensation.valueChanged.connect(self.setExposureCompensation) self.setCamera(QMediaDevices.defaultVideoInput()) @@ -278,10 +277,6 @@ class Camera(QMainWindow): self._ui.stopButton.setEnabled(True) self._ui.metaDataButton.setEnabled(False) - @Slot(int) - def setExposureCompensation(self, index): - self.m_camera.setExposureCompensation(index * 0.5) - @Slot() def displayRecorderError(self): if self.m_mediaRecorder.error() != QMediaRecorder.NoError: diff --git a/examples/multimedia/camera/camera.ui b/examples/multimedia/camera/camera.ui index a338fb51..1852c3e9 100644 --- a/examples/multimedia/camera/camera.ui +++ b/examples/multimedia/camera/camera.ui @@ -48,32 +48,6 @@ - - - - -4 - - - 4 - - - 2 - - - Qt::Orientation::Horizontal - - - QSlider::TickPosition::TicksAbove - - - - - - - Exposure Compensation: - - - @@ -283,7 +257,7 @@ - Quit + Close Ctrl+Q @@ -416,22 +390,6 @@ - - exposureCompensation - valueChanged(int) - Camera - setExposureCompensation(int) - - - 559 - 367 - - - 665 - 365 - - - actionSettings triggered() diff --git a/examples/multimedia/camera/ui_camera.py b/examples/multimedia/camera/ui_camera.py index db78ecf5..ec7dfcec 100644 --- a/examples/multimedia/camera/ui_camera.py +++ b/examples/multimedia/camera/ui_camera.py @@ -19,8 +19,8 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, from PySide6.QtMultimediaWidgets import QVideoWidget from PySide6.QtWidgets import (QApplication, QFrame, QGridLayout, QLabel, QMainWindow, QMenu, QMenuBar, QPushButton, - QSizePolicy, QSlider, QSpacerItem, QStackedWidget, - QStatusBar, QTabWidget, QWidget) + QSizePolicy, QSpacerItem, QStackedWidget, QStatusBar, + QTabWidget, QWidget) class Ui_Camera(object): def setupUi(self, Camera): @@ -57,21 +57,6 @@ class Ui_Camera(object): self.gridLayout.addWidget(self.takeImageButton, 0, 0, 1, 1) - self.exposureCompensation = QSlider(self.tab_2) - self.exposureCompensation.setObjectName(u"exposureCompensation") - self.exposureCompensation.setMinimum(-4) - self.exposureCompensation.setMaximum(4) - self.exposureCompensation.setPageStep(2) - self.exposureCompensation.setOrientation(Qt.Orientation.Horizontal) - self.exposureCompensation.setTickPosition(QSlider.TickPosition.TicksAbove) - - self.gridLayout.addWidget(self.exposureCompensation, 5, 0, 1, 1) - - self.label = QLabel(self.tab_2) - self.label.setObjectName(u"label") - - self.gridLayout.addWidget(self.label, 4, 0, 1, 1) - self.captureWidget.addTab(self.tab_2, "") self.tab = QWidget() self.tab.setObjectName(u"tab") @@ -121,15 +106,15 @@ class Ui_Camera(object): self.stackedWidget.setSizePolicy(sizePolicy) palette = QPalette() brush = QBrush(QColor(255, 255, 255, 255)) - brush.setStyle(Qt.SolidPattern) - palette.setBrush(QPalette.Active, QPalette.Base, brush) + brush.setStyle(Qt.BrushStyle.SolidPattern) + palette.setBrush(QPalette.ColorGroup.Active, QPalette.ColorRole.Base, brush) brush1 = QBrush(QColor(145, 145, 145, 255)) - brush1.setStyle(Qt.SolidPattern) - palette.setBrush(QPalette.Active, QPalette.Window, brush1) - palette.setBrush(QPalette.Inactive, QPalette.Base, brush) - palette.setBrush(QPalette.Inactive, QPalette.Window, brush1) - palette.setBrush(QPalette.Disabled, QPalette.Base, brush1) - palette.setBrush(QPalette.Disabled, QPalette.Window, brush1) + brush1.setStyle(Qt.BrushStyle.SolidPattern) + palette.setBrush(QPalette.ColorGroup.Active, QPalette.ColorRole.Window, brush1) + palette.setBrush(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Base, brush) + palette.setBrush(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Window, brush1) + palette.setBrush(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Base, brush1) + palette.setBrush(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Window, brush1) self.stackedWidget.setPalette(palette) self.viewfinderPage = QWidget() self.viewfinderPage.setObjectName(u"viewfinderPage") @@ -193,7 +178,6 @@ class Ui_Camera(object): self.actionExit.triggered.connect(Camera.close) self.takeImageButton.clicked.connect(Camera.takeImage) self.muteButton.toggled.connect(Camera.setMuted) - self.exposureCompensation.valueChanged.connect(Camera.setExposureCompensation) self.actionSettings.triggered.connect(Camera.configureCaptureSettings) self.actionStartCamera.triggered.connect(Camera.startCamera) self.actionStopCamera.triggered.connect(Camera.stopCamera) @@ -207,7 +191,7 @@ class Ui_Camera(object): def retranslateUi(self, Camera): Camera.setWindowTitle(QCoreApplication.translate("Camera", u"Camera", None)) - self.actionExit.setText(QCoreApplication.translate("Camera", u"Quit", None)) + self.actionExit.setText(QCoreApplication.translate("Camera", u"Close", None)) #if QT_CONFIG(shortcut) self.actionExit.setShortcut(QCoreApplication.translate("Camera", u"Ctrl+Q", None)) #endif // QT_CONFIG(shortcut) @@ -216,7 +200,6 @@ class Ui_Camera(object): self.actionSettings.setText(QCoreApplication.translate("Camera", u"Change Settings", None)) self.actionAbout_Qt.setText(QCoreApplication.translate("Camera", u"About Qt", None)) self.takeImageButton.setText(QCoreApplication.translate("Camera", u"Capture Photo", None)) - self.label.setText(QCoreApplication.translate("Camera", u"Exposure Compensation:", None)) self.captureWidget.setTabText(self.captureWidget.indexOf(self.tab_2), QCoreApplication.translate("Camera", u"Image", None)) self.recordButton.setText(QCoreApplication.translate("Camera", u"Record", None)) self.pauseButton.setText(QCoreApplication.translate("Camera", u"Pause", None)) diff --git a/examples/multimedia/camera/ui_camera_mobile.py b/examples/multimedia/camera/ui_camera_mobile.py index a9bd4d09..eff3310a 100644 --- a/examples/multimedia/camera/ui_camera_mobile.py +++ b/examples/multimedia/camera/ui_camera_mobile.py @@ -144,15 +144,15 @@ class Ui_Camera(object): self.stackedWidget.setSizePolicy(sizePolicy2) palette = QPalette() brush = QBrush(QColor(255, 255, 255, 255)) - brush.setStyle(Qt.SolidPattern) - palette.setBrush(QPalette.Active, QPalette.Base, brush) + brush.setStyle(Qt.BrushStyle.SolidPattern) + palette.setBrush(QPalette.ColorGroup.Active, QPalette.ColorRole.Base, brush) brush1 = QBrush(QColor(145, 145, 145, 255)) - brush1.setStyle(Qt.SolidPattern) - palette.setBrush(QPalette.Active, QPalette.Window, brush1) - palette.setBrush(QPalette.Inactive, QPalette.Base, brush) - palette.setBrush(QPalette.Inactive, QPalette.Window, brush1) - palette.setBrush(QPalette.Disabled, QPalette.Base, brush1) - palette.setBrush(QPalette.Disabled, QPalette.Window, brush1) + brush1.setStyle(Qt.BrushStyle.SolidPattern) + palette.setBrush(QPalette.ColorGroup.Active, QPalette.ColorRole.Window, brush1) + palette.setBrush(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Base, brush) + palette.setBrush(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Window, brush1) + palette.setBrush(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Base, brush1) + palette.setBrush(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Window, brush1) self.stackedWidget.setPalette(palette) self.viewfinderPage = QWidget() self.viewfinderPage.setObjectName(u"viewfinderPage") diff --git a/examples/multimedia/player/audiolevelmeter.py b/examples/multimedia/player/audiolevelmeter.py new file mode 100644 index 00000000..d961a392 --- /dev/null +++ b/examples/multimedia/player/audiolevelmeter.py @@ -0,0 +1,387 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from math import log10, sqrt +from PySide6.QtMultimedia import QAudioBuffer +from PySide6.QtWidgets import (QApplication, QHBoxLayout, QLabel, QSizePolicy, QToolButton, + QVBoxLayout, QWidget) +from PySide6.QtGui import QBrush, QPainter, QPalette +from PySide6.QtCore import QObject, QRectF, QThread, QTimer, qFuzzyCompare, Qt, Signal, Slot + + +# Constants used by AudioLevelMeter and MeterChannel +WIDGET_WIDTH = 34 +MAX_CHANNELS = 8 +PEAK_COLOR = "#1F9B5D" +RMS_COLOR = "#28C878" +RMS_WINDOW = 400 # ms +PEAK_LABEL_HOLD_TIME = 2000 # ms +DECAY_EASE_IN_TIME = 160 # ms +UPDATE_INTERVAL = 16 # ms, Assuming 60 Hz refresh rate. +DB_DECAY_PER_SECOND = 20.0 +DB_DECAY_PER_UPDATE = DB_DECAY_PER_SECOND / (1000 / UPDATE_INTERVAL) +DB_MAX = 0.0 +DB_MIN = -60.0 + + +def amplitudeToDb(f): + """Converts a float sample value to dB and clamps it between DB_MIN and DB_MAX.""" + if f <= 0: + return DB_MIN + v = 20.0 * log10(f) + if v < DB_MIN: + return DB_MIN + if v > DB_MAX: + return DB_MAX + return v + + +# A struct used by BufferAnalyzer to emit its results back to AudioLevelMeter +class BufferValues: + """A struct used by BufferAnalyzer to emit its results back to AudioLevelMeter.""" + def __init__(self, nChannels): + self.peaks = [0.0] * nChannels + self.squares = [0.0] * nChannels + + +class BufferAnalyzer(QObject): + """A worker class analyzing incoming buffers on a separate worker thread.""" + valuesReady = Signal(BufferValues) + + def __init__(self, parent=None): + super().__init__(parent) + self.m_stopRequested = False + + def requestStop(self): + self.m_stopRequested = True + + @Slot(QAudioBuffer, int) + def analyzeBuffer(self, buffer, maxChannelsToAnalyze): + """Analyzes an audio buffer and emits its peak and sumOfSquares values. + Skips remaining frames if self.m_stopRequested is set to true.""" + + if QThread.currentThread().isInterruptionRequested(): + return # Interrupted by ~AudioLevelMeter, skipping remaining buffers in signal queue + + self.m_stopRequested = False + + channelCount = buffer.format().channelCount() + channelsToAnalyze = min(channelCount, maxChannelsToAnalyze) + + values = BufferValues(channelsToAnalyze) + + bufferData = buffer.constData() + bufferSize = len(bufferData) + bytesPerSample = buffer.format().bytesPerSample() + + for i in range(0, bufferSize, bytesPerSample * channelCount): + if self.m_stopRequested: + framesSkipped = (bufferSize - i) / channelCount + print("BufferAnalyzer::analyzeBuffer skipped", framesSkipped, "out of", + buffer.frameCount(), "frames") + # Emit incomplete values also when stop is requested to get some audio level readout + # even if frames are being skipped for every buffer. Displayed levels will be + # inaccurate. + break + + for channelIndex in range(0, channelsToAnalyze): + offset = i + bytesPerSample * channelIndex + sample = buffer.format().normalizedSampleValue(bufferData[offset:]) + values.peaks[channelIndex] = max(values.peaks[channelIndex], abs(sample)) + values.squares[channelIndex] += sample * sample + + self.valuesReady.emit(values) + + +class MeterChannel(QWidget): + """A custom QWidget representing an audio channel in the audio level meter. It serves + both as a model for the channels's peak and RMS values and as a view using the overridden + paintEvent().""" + def __init__(self, parent): + super().__init__(parent) + self.m_peakDecayRate = 0.0 + self.m_rmsDecayRate = 0.0 + self.m_peak = DB_MIN + self.m_rms = DB_MIN + self.m_sumOfSquares = 0.0 + self.m_sumOfSquaresQueue = [] + self.m_peakBrush = QBrush(PEAK_COLOR) + self.m_rmsBrush = QBrush(RMS_COLOR) + + def normalize(self, dB): + """# Normalizes a dB value for visualization.""" + return (dB - DB_MIN) / (DB_MAX - DB_MIN) + + def clearRmsData(self): + """Clears the data used to calculate RMS values.""" + self.m_sumOfSquares = 0.0 + self.m_sumOfSquaresQueue = [] + + def decayPeak(self): + """Decays self.m_peak value by DB_DECAY_PER_UPDATE with ease-in animation based + on DECAY_EASE_IN_TIME.""" + peak = self.m_peak + if qFuzzyCompare(peak, DB_MIN): + return + + cubicEaseInFactor = self.m_peakDecayRate * self.m_peakDecayRate * self.m_peakDecayRate + self.m_peak = max(DB_MIN, peak - DB_DECAY_PER_UPDATE * cubicEaseInFactor) + + if self.m_peakDecayRate < 1: + self.m_peakDecayRate += float(UPDATE_INTERVAL) / float(DECAY_EASE_IN_TIME) + if self.m_peakDecayRate > 1.0: + self.m_peakDecayRate = 1.0 + + def decayRms(self): + """Decays self.m_rms value by DB_DECAY_PER_UPDATE with ease-in animation based on + DECAY_EASE_IN_TIME.""" + rms = self.m_rms + if qFuzzyCompare(rms, DB_MIN): + return + + cubicEaseInFactor = self.m_rmsDecayRate * self.m_rmsDecayRate * self.m_rmsDecayRate + self.m_rms = max(DB_MIN, rms - DB_DECAY_PER_UPDATE * cubicEaseInFactor) + + if self.m_rmsDecayRate < 1: + self.m_rmsDecayRate += float(UPDATE_INTERVAL) / float(DECAY_EASE_IN_TIME) + if self.m_rmsDecayRate > 1.0: + self.m_rmsDecayRate = 1.0 + + def updatePeak(self, sampleValue): + """Updates self.m_peak and resets self.m_peakDecayRate if sampleValue > self.m_peak.""" + dB = amplitudeToDb(sampleValue) + if dB > self.m_peak: + self.m_peakDecayRate = 0 + self.m_peak = dB + + def updateRms(self, sumOfSquaresForOneBuffer, duration, frameCount): + """Calculates current RMS. Resets self.m_rmsDecayRate and updates self.m_rms + if current RMS > self.m_rms.""" + + # Add the new sumOfSquares to the queue and update the total + self.m_sumOfSquaresQueue.append(sumOfSquaresForOneBuffer) + self.m_sumOfSquares += sumOfSquaresForOneBuffer + + # Remove the oldest sumOfSquares to stay within the RMS window + if len(self.m_sumOfSquaresQueue) * duration > RMS_WINDOW: + self.m_sumOfSquares -= self.m_sumOfSquaresQueue[0] + del self.m_sumOfSquaresQueue[0] + + # Fix negative values caused by floating point precision errors + if self.m_sumOfSquares < 0: + self.m_sumOfSquares = 0 + + # Calculate the new RMS value + if self.m_sumOfSquares > 0 and self.m_sumOfSquaresQueue: + newRms = sqrt(self.m_sumOfSquares / (frameCount * len(self.m_sumOfSquaresQueue))) + dB = amplitudeToDb(newRms) + if dB > self.m_rms: + self.m_rmsDecayRate = 0 + self.m_rms = dB + + def paintEvent(self, event): + """Paints the level bar of the meter channel based on the decayed peak and rms values.""" + if qFuzzyCompare(self.m_peak, DB_MIN) and qFuzzyCompare(self.m_rms, DB_MIN): + return # Nothing to paint + + peakLevel = self.normalize(self.m_peak) + rmsLevel = self.normalize(self.m_rms) + + with QPainter(self) as painter: + rect = QRectF(0, self.height(), self.width(), -peakLevel * self.height()) + painter.fillRect(rect, self.m_peakBrush) # Paint the peak level + rect.setHeight(-rmsLevel * self.height()) + painter.fillRect(rect, self.m_rmsBrush) # Paint the RMS level + + +class AudioLevelMeter(QWidget): + """The audio level meter´s parent widget class. It acts as a controller + for the MeterChannel widgets and the BufferAnalyzer worker.""" + + newBuffer = Signal(QAudioBuffer, int) + + def __init__(self, parent=None): + super().__init__(parent) + self.m_isOn = True + self.m_isActive = False + self.m_channels = [] + self.m_channelCount = 0 + self.m_bufferDurationMs = 0 + self.m_frameCount = 0 + self.m_highestPeak = 0.0 + self.m_updateTimer = QTimer() + self.m_deactivationTimer = QTimer() + self.m_peakLabelHoldTimer = QTimer() + self.m_peakLabel = None + self.m_onOffButton = None + self.m_bufferAnalyzer = None + self.m_analyzerThread = QThread() + + # Layout and background color + self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) + self.setMinimumWidth(WIDGET_WIDTH) + currentPalette = self.palette() + currentPalette.setColor(QPalette.ColorRole.Window, + currentPalette.color(QPalette.ColorRole.Base)) + self.setPalette(currentPalette) + self.setAutoFillBackground(True) + mainLayout = QVBoxLayout(self) + mainLayout.setSpacing(2) + mainLayout.setContentsMargins(0, 0, 0, 0) + + # Meter channels + meterChannelLayout = QHBoxLayout() + meterChannelLayout.setContentsMargins(2, 2, 2, 2) + meterChannelLayout.setSpacing(2) + for i in range(0, MAX_CHANNELS): + channel = MeterChannel(self) + meterChannelLayout.addWidget(channel) + self.m_channels.append(channel) + mainLayout.addLayout(meterChannelLayout) + + # Peak label + self.m_peakLabel = QLabel("-", self) + self.m_peakLabel.setAlignment(Qt.AlignmentFlag.AlignCenter) + font = QApplication.font() + font.setPointSize(10) + self.m_peakLabel.setFont(font) + mainLayout.addWidget(self.m_peakLabel) + mainLayout.setStretch(0, 1) + + # On/off button + self.m_onOffButton = QToolButton(self) + mainLayout.addWidget(self.m_onOffButton) + self.m_onOffButton.setMaximumWidth(WIDGET_WIDTH) + self.m_onOffButton.setText("On") + self.m_onOffButton.setCheckable(True) + self.m_onOffButton.setChecked(True) + self.m_onOffButton.clicked.connect(self.toggleOnOff) + + # Timer triggering update of the audio level bars + self.m_updateTimer.timeout.connect(self.updateBars) + + # Timer postponing deactivation of update timer to allow meters to fade to 0 + self.m_deactivationTimer.timeout.connect(self.m_updateTimer.stop) + self.m_deactivationTimer.setSingleShot(True) + + # Timer resetting the peak label + self.m_peakLabelHoldTimer.timeout.connect(self.resetPeakLabel) + self.m_peakLabelHoldTimer.setSingleShot(True) + + # Buffer analyzer and worker thread that analyzes incoming buffers + self.m_bufferAnalyzer = BufferAnalyzer() + self.m_bufferAnalyzer.moveToThread(self.m_analyzerThread) + self.m_analyzerThread.finished.connect(self.m_bufferAnalyzer.deleteLater) + self.newBuffer.connect(self.m_bufferAnalyzer.analyzeBuffer) + self.m_bufferAnalyzer.valuesReady.connect(self.updateValues) + self.m_analyzerThread.start() + + def closeRequest(self): + self.m_analyzerThread.requestInterruption() + self.m_bufferAnalyzer.requestStop() + self.m_analyzerThread.quit() + self.m_analyzerThread.wait() + + @Slot(QAudioBuffer) + def onAudioBufferReceived(self, buffer): + """Receives a buffer from QAudioBufferOutput and triggers BufferAnalyzer to analyze it.""" + if not self.m_isOn or not buffer.isValid() or not buffer.format().isValid(): + return + + if not self.m_isActive: + self.activate() + + # Update internal values to match the current audio stream + self.updateChannelCount(buffer.format().channelCount()) + self.m_frameCount = buffer.frameCount() + self.m_bufferDurationMs = buffer.duration() / 1000 + + # Stop any ongoing analysis, skipping remaining frames + self.m_bufferAnalyzer.requestStop() + + self.newBuffer.emit(buffer, self.m_channelCount) + + @Slot(BufferValues) + def updateValues(self, values): + """Updates peak/RMS values and peak label.""" + if not self.m_isActive: + return # Discard incoming values from BufferAnalyzer + + bufferPeak = 0.0 + for i in range(0, len(values.peaks)): + bufferPeak = max(bufferPeak, values.peaks[i]) + self.m_channels[i].updatePeak(values.peaks[i]) + self.m_channels[i].updateRms(values.squares[i], self.m_bufferDurationMs, + self.m_frameCount) + self.updatePeakLabel(bufferPeak) + + def updatePeakLabel(self, peak): + """Updates peak label and restarts self.m_peakLabelHoldTimer + if peak >= self.m_highestPeak.""" + if peak < self.m_highestPeak: + return + + self.m_peakLabelHoldTimer.start(PEAK_LABEL_HOLD_TIME) + + if qFuzzyCompare(peak, self.m_highestPeak): + return + + self.m_highestPeak = peak + dB = amplitudeToDb(self.m_highestPeak) + self.m_peakLabel.setText(f"{int(dB)}") + + @Slot() + def resetPeakLabel(self): + """Resets peak label. Called when self.m_labelHoldTimer timeouts.""" + self.m_highestPeak = 0.0 + self.m_peakLabel.setText(f"{DB_MIN}" if self.m_isOn else "") + + def clearAllRmsData(self): + """Clears internal data used to calculate RMS values.""" + for channel in self.m_channels.copy(): + channel.clearRmsData() + + @Slot() + def activate(self): + """Starts the update timer that updates the meter bar.""" + self.m_isActive = True + self.m_deactivationTimer.stop() + self.m_updateTimer.start(UPDATE_INTERVAL) + + @Slot() + def deactivate(self): + """Start the deactiviation timer that eventually stops the update timer.""" + self.m_isActive = False + self.clearAllRmsData() + # Calculate the time it takes to decay fram max to min dB + interval = (DB_MAX - DB_MIN) / (DB_DECAY_PER_SECOND / 1000) + DECAY_EASE_IN_TIME + self.m_deactivationTimer.start(interval) + + @Slot() + def updateBars(self): + """Decays internal peak and RMS values and triggers repainting of meter bars.""" + for i in range(0, self.m_channelCount): + channel = self.m_channels[i] + channel.decayPeak() + channel.decayRms() + channel.update() # Trigger paint event + + @Slot() + def toggleOnOff(self): + """Toggles between on (activated) and off (deactivated) state.""" + self.m_isOn = not self.m_isOn + if not self.m_isOn: + self.deactivate() + else: + self.activate() + self.m_onOffButton.setText("On" if self.m_isOn else "Off") + + def updateChannelCount(self, channelCount): + """Updates the number of visible MeterChannel widgets.""" + if (channelCount == self.m_channelCount + or (channelCount > MAX_CHANNELS and MAX_CHANNELS == self.m_channelCount)): + return + + self.m_channelCount = min(channelCount, MAX_CHANNELS) + for i in range(0, MAX_CHANNELS): + self.m_channels[i].setVisible(i < self.m_channelCount) diff --git a/examples/multimedia/player/doc/player.png b/examples/multimedia/player/doc/player.png deleted file mode 100644 index f751d4a8..00000000 Binary files a/examples/multimedia/player/doc/player.png and /dev/null differ diff --git a/examples/multimedia/player/doc/player.rst b/examples/multimedia/player/doc/player.rst index fdf5fa92..77d05122 100644 --- a/examples/multimedia/player/doc/player.rst +++ b/examples/multimedia/player/doc/player.rst @@ -4,6 +4,6 @@ Player Example Media Player demonstrates a simple multimedia player that can play audio and or video files using various codecs. -.. image:: player.png - :width: 400 +.. image:: player.webp + :width: 800 :alt: Player Screenshot diff --git a/examples/multimedia/player/doc/player.webp b/examples/multimedia/player/doc/player.webp new file mode 100644 index 00000000..5a52c66b Binary files /dev/null and b/examples/multimedia/player/doc/player.webp differ diff --git a/examples/multimedia/player/main.py b/examples/multimedia/player/main.py new file mode 100644 index 00000000..2afe7626 --- /dev/null +++ b/examples/multimedia/player/main.py @@ -0,0 +1,31 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +"""PySide6 Multimedia player example""" + +import sys +from argparse import ArgumentParser, RawTextHelpFormatter + +from PySide6.QtWidgets import QApplication +from PySide6.QtCore import qVersion, QCoreApplication, QDir, QUrl + +from player import Player + + +if __name__ == "__main__": + app = QApplication(sys.argv) + + QCoreApplication.setApplicationName("Player Example") + QCoreApplication.setOrganizationName("QtProject") + QCoreApplication.setApplicationVersion(qVersion()) + argument_parser = ArgumentParser(description=QCoreApplication.applicationName(), + formatter_class=RawTextHelpFormatter) + argument_parser.add_argument("file", help="File", nargs='?', type=str) + options = argument_parser.parse_args() + + player = Player() + if options.file: + player.openUrl(QUrl.fromUserInput(options.file, QDir.currentPath(), + QUrl.UserInputResolutionOption.AssumeLocalFile)) + player.show() + sys.exit(QCoreApplication.exec()) diff --git a/examples/multimedia/player/player.py b/examples/multimedia/player/player.py index 23fdbb4c..57e0112c 100644 --- a/examples/multimedia/player/player.py +++ b/examples/multimedia/player/player.py @@ -1,202 +1,456 @@ -# Copyright (C) 2022 The Qt Company Ltd. +# Copyright (C) 2025 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from __future__ import annotations -"""PySide6 Multimedia player example""" +from functools import cache -import sys -from PySide6.QtCore import QStandardPaths, Qt, Slot -from PySide6.QtGui import QAction, QIcon, QKeySequence -from PySide6.QtWidgets import (QApplication, QDialog, QFileDialog, - QMainWindow, QSlider, QStyle, QToolBar) -from PySide6.QtMultimedia import (QAudioOutput, QMediaFormat, - QMediaPlayer, QAudio) -from PySide6.QtMultimediaWidgets import QVideoWidget +from PySide6.QtMultimedia import (QAudioBufferOutput, QAudioDevice, QAudioOutput, QMediaDevices, + QMediaFormat, QMediaMetaData, QMediaPlayer) +from PySide6.QtWidgets import (QApplication, QComboBox, QDialog, QFileDialog, QGridLayout, + QHBoxLayout, QLabel, QLineEdit, QMessageBox, QPushButton, + QSizePolicy, QSlider, QVBoxLayout, QWidget) +from PySide6.QtGui import QCursor, QPixmap +from PySide6.QtCore import QDir, QLocale, QStandardPaths, QTime, Qt, Signal, Slot - -AVI = "video/x-msvideo" # AVI +from audiolevelmeter import AudioLevelMeter +from playercontrols import PlayerControls +from videowidget import VideoWidget MP4 = 'video/mp4' -def get_supported_mime_types(): +@cache +def getSupportedMimeTypes(): result = [] - for f in QMediaFormat().supportedFileFormats(QMediaFormat.Decode): + for f in QMediaFormat().supportedFileFormats(QMediaFormat.ConversionMode.Decode): mime_type = QMediaFormat(f).mimeType() result.append(mime_type.name()) + if MP4 not in result: + result.append(MP4) # Should always be there when using FFMPEG return result -class MainWindow(QMainWindow): - - def __init__(self): - super().__init__() - - self._playlist = [] # FIXME 6.3: Replace by QMediaPlaylist? - self._playlist_index = -1 - self._audio_output = QAudioOutput() - self._player = QMediaPlayer() - self._player.setAudioOutput(self._audio_output) - - self._player.errorOccurred.connect(self._player_error) - - tool_bar = QToolBar() - self.addToolBar(tool_bar) - - file_menu = self.menuBar().addMenu("&File") - icon = QIcon.fromTheme(QIcon.ThemeIcon.DocumentOpen) - open_action = QAction(icon, "&Open...", self, - shortcut=QKeySequence.Open, triggered=self.open) - file_menu.addAction(open_action) - tool_bar.addAction(open_action) - icon = QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit) - exit_action = QAction(icon, "E&xit", self, - shortcut="Ctrl+Q", triggered=self.close) - file_menu.addAction(exit_action) - - play_menu = self.menuBar().addMenu("&Play") - style = self.style() - icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart, - style.standardIcon(QStyle.SP_MediaPlay)) - self._play_action = tool_bar.addAction(icon, "Play") - self._play_action.triggered.connect(self._player.play) - play_menu.addAction(self._play_action) - - icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaSkipBackward, - style.standardIcon(QStyle.SP_MediaSkipBackward)) - self._previous_action = tool_bar.addAction(icon, "Previous") - self._previous_action.triggered.connect(self.previous_clicked) - play_menu.addAction(self._previous_action) - - icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackPause, - style.standardIcon(QStyle.SP_MediaPause)) - self._pause_action = tool_bar.addAction(icon, "Pause") - self._pause_action.triggered.connect(self._player.pause) - play_menu.addAction(self._pause_action) - - icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaSkipForward, - style.standardIcon(QStyle.SP_MediaSkipForward)) - self._next_action = tool_bar.addAction(icon, "Next") - self._next_action.triggered.connect(self.next_clicked) - play_menu.addAction(self._next_action) - - icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStop, - style.standardIcon(QStyle.SP_MediaStop)) - self._stop_action = tool_bar.addAction(icon, "Stop") - self._stop_action.triggered.connect(self._ensure_stopped) - play_menu.addAction(self._stop_action) - - self._volume_slider = QSlider() - self._volume_slider.setOrientation(Qt.Orientation.Horizontal) - self._volume_slider.setMinimum(0) - self._volume_slider.setMaximum(100) - available_width = self.screen().availableGeometry().width() - self._volume_slider.setFixedWidth(available_width / 10) - self._volume_slider.setValue(self._audio_output.volume() * 100) - self._volume_slider.setTickInterval(10) - self._volume_slider.setTickPosition(QSlider.TicksBelow) - self._volume_slider.setToolTip("Volume") - self._volume_slider.valueChanged.connect(self.setVolume) - tool_bar.addWidget(self._volume_slider) - - icon = QIcon.fromTheme(QIcon.ThemeIcon.HelpAbout) - about_menu = self.menuBar().addMenu("&About") - about_qt_act = QAction(icon, "About &Qt", self, triggered=qApp.aboutQt) # noqa: F821 - about_menu.addAction(about_qt_act) - - self._video_widget = QVideoWidget() - self.setCentralWidget(self._video_widget) - self._player.playbackStateChanged.connect(self.update_buttons) - self._player.setVideoOutput(self._video_widget) - - self.update_buttons(self._player.playbackState()) - self._mime_types = [] +class Player(QWidget): + + fullScreenChanged = Signal(bool) + + def __init__(self, parent=None): + super().__init__(parent) + self.m_statusInfo = "" + self.m_mediaDevices = QMediaDevices() + self.m_player = QMediaPlayer(self) + self.m_audioOutput = QAudioOutput(self) + self.m_player.setAudioOutput(self.m_audioOutput) + self.m_player.durationChanged.connect(self.durationChanged) + self.m_player.positionChanged.connect(self.positionChanged) + self.m_player.metaDataChanged.connect(self.metaDataChanged) + self.m_player.mediaStatusChanged.connect(self.statusChanged) + self.m_player.bufferProgressChanged.connect(self.bufferingProgress) + self.m_player.hasVideoChanged.connect(self.videoAvailableChanged) + self.m_player.errorChanged.connect(self.displayErrorMessage) + self.m_player.tracksChanged.connect(self.tracksChanged) + + self.m_videoWidget = VideoWidget(self) + available_geometry = self.screen().availableGeometry() + self.m_videoWidget.setMinimumSize(available_geometry.width() / 2, + available_geometry.height() / 3) + self.m_player.setVideoOutput(self.m_videoWidget) + + # audio level meter + self.m_audioBufferOutput = QAudioBufferOutput(self) + self.m_player.setAudioBufferOutput(self.m_audioBufferOutput) + self.m_audioLevelMeter = AudioLevelMeter(self) + self.m_audioBufferOutput.audioBufferReceived.connect(self.m_audioLevelMeter.onAudioBufferReceived) # noqa: E501 + self.m_player.playingChanged.connect(self.m_audioLevelMeter.deactivate) + + # player layout + layout = QVBoxLayout(self) + + # display + displayLayout = QHBoxLayout() + displayLayout.addWidget(self.m_videoWidget, 2) + displayLayout.addWidget(self.m_audioLevelMeter, 3) + layout.addLayout(displayLayout) + + # duration slider and label + hLayout = QHBoxLayout() + + self.m_slider = QSlider(Qt.Orientation.Horizontal, self) + self.m_slider.setRange(0, self.m_player.duration()) + self.m_slider.sliderMoved.connect(self.seek) + hLayout.addWidget(self.m_slider) + + self.m_labelDuration = QLabel() + self.m_labelDuration.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + hLayout.addWidget(self.m_labelDuration) + layout.addLayout(hLayout) + + # controls + controlLayout = QHBoxLayout() + controlLayout.setContentsMargins(0, 0, 0, 0) + + openButton = QPushButton("Open", self) + openButton.clicked.connect(self.open) + controlLayout.addWidget(openButton) + controlLayout.addStretch(1) + + controls = PlayerControls() + controls.setState(self.m_player.playbackState()) + controls.setVolume(self.m_audioOutput.volume()) + controls.setMuted(controls.isMuted()) + + controls.play.connect(self.m_player.play) + controls.pause.connect(self.m_player.pause) + controls.stop.connect(self.m_player.stop) + controls.previous.connect(self.previousClicked) + controls.changeVolume.connect(self.m_audioOutput.setVolume) + controls.changeMuting.connect(self.m_audioOutput.setMuted) + controls.changeRate.connect(self.m_player.setPlaybackRate) + controls.stop.connect(self.m_videoWidget.update) + + self.m_player.playbackStateChanged.connect(controls.setState) + self.m_audioOutput.volumeChanged.connect(controls.setVolume) + self.m_audioOutput.mutedChanged.connect(controls.setMuted) + + controlLayout.addWidget(controls) + controlLayout.addStretch(1) + + self.m_fullScreenButton = QPushButton("FullScreen", self) + self.m_fullScreenButton.setCheckable(True) + controlLayout.addWidget(self.m_fullScreenButton) + + self.m_pitchCompensationButton = QPushButton("Pitch compensation", self) + self.m_pitchCompensationButton.setCheckable(True) + av = self.m_player.pitchCompensationAvailability() + toolTip = "" + if av == QMediaPlayer.PitchCompensationAvailability.AlwaysOn: + self.m_pitchCompensationButton.setEnabled(False) + self.m_pitchCompensationButton.setChecked(True) + toolTip = "Pitch compensation always enabled on self backend" + elif av == QMediaPlayer.PitchCompensationAvailability.Unavailable: + self.m_pitchCompensationButton.setEnabled(False) + self.m_pitchCompensationButton.setChecked(False) + toolTip = "Pitch compensation unavailable on self backend" + elif av == QMediaPlayer.PitchCompensationAvailability.Available: + self.m_pitchCompensationButton.setEnabled(True) + self.m_pitchCompensationButton.setChecked(self.m_player.pitchCompensation()) + self.m_pitchCompensationButton.setToolTip(toolTip) + + controlLayout.addWidget(self.m_pitchCompensationButton) + self.m_player.pitchCompensationChanged.connect(self._updatePitchCompensation) + self.m_pitchCompensationButton.setChecked(self.m_player.pitchCompensation()) + self.m_pitchCompensationButton.toggled.connect(self.m_player.setPitchCompensation) + + self.m_audioOutputCombo = QComboBox(self) + controlLayout.addWidget(self.m_audioOutputCombo) + + self.updateAudioDevices() + + self.m_audioOutputCombo.activated.connect(self.audioOutputChanged) + + self.m_mediaDevices.audioOutputsChanged.connect(self.updateAudioDevices) + + layout.addLayout(controlLayout) + + # tracks + tracksLayout = QGridLayout() + + self.m_audioTracks = QComboBox(self) + self.m_audioTracks.activated.connect(self.selectAudioStream) + tracksLayout.addWidget(QLabel("Audio Tracks:"), 0, 0) + tracksLayout.addWidget(self.m_audioTracks, 0, 1) + + self.m_videoTracks = QComboBox(self) + self.m_videoTracks.activated.connect(self.selectVideoStream) + tracksLayout.addWidget(QLabel("Video Tracks:"), 1, 0) + tracksLayout.addWidget(self.m_videoTracks, 1, 1) + + self.m_subtitleTracks = QComboBox(self) + self.m_subtitleTracks.activated.connect(self.selectSubtitleStream) + tracksLayout.addWidget(QLabel("Subtitle Tracks:"), 2, 0) + tracksLayout.addWidget(self.m_subtitleTracks, 2, 1) + + layout.addLayout(tracksLayout) + + # metadata + metaDataLabel = QLabel("Metadata for file:") + layout.addWidget(metaDataLabel) + + metaDataLayout = QGridLayout() + metaDataCount = QMediaMetaData.NumMetaData + self.m_metaDataLabels = [None] * metaDataCount + self.m_metaDataFields = [None] * metaDataCount + key = QMediaMetaData.Key.Title.value + for i in range(0, round((metaDataCount + 2) / 3)): + for j in range(0, 6, 2): + labelText = QMediaMetaData.metaDataKeyToString(QMediaMetaData.Key(key)) + self.m_metaDataLabels[key] = QLabel(labelText) + if (key == QMediaMetaData.Key.ThumbnailImage + or key == QMediaMetaData.Key.CoverArtImage): + self.m_metaDataFields[key] = QLabel() + else: + lineEdit = QLineEdit() + lineEdit.setReadOnly(True) + self.m_metaDataFields[key] = lineEdit + + self.m_metaDataLabels[key].setDisabled(True) + self.m_metaDataFields[key].setDisabled(True) + metaDataLayout.addWidget(self.m_metaDataLabels[key], i, j) + metaDataLayout.addWidget(self.m_metaDataFields[key], i, j + 1) + key += 1 + if key == QMediaMetaData.NumMetaData: + break + + layout.addLayout(metaDataLayout) + + if not self.isPlayerAvailable(): + QMessageBox.warning(self, "Service not available", + "The QMediaPlayer object does not have a valid service.\n" + "Please check the media service plugins are installed.") + + controls.setEnabled(False) + openButton.setEnabled(False) + self.m_fullScreenButton.setEnabled(False) + self.metaDataChanged() def closeEvent(self, event): - self._ensure_stopped() + self.m_audioLevelMeter.closeRequest() event.accept() + @Slot() + def _updatePitchCompensation(self): + self.m_pitchCompensationButton.setChecked(self.m_player.pitchCompensation()) + + def isPlayerAvailable(self): + return self.m_player.isAvailable() + @Slot() def open(self): - self._ensure_stopped() - file_dialog = QFileDialog(self) - - is_windows = sys.platform == 'win32' - if not self._mime_types: - self._mime_types = get_supported_mime_types() - if (is_windows and AVI not in self._mime_types): - self._mime_types.append(AVI) - elif MP4 not in self._mime_types: - self._mime_types.append(MP4) - - file_dialog.setMimeTypeFilters(self._mime_types) - - default_mimetype = AVI if is_windows else MP4 - if default_mimetype in self._mime_types: - file_dialog.selectMimeTypeFilter(default_mimetype) - - movies_location = QStandardPaths.writableLocation(QStandardPaths.MoviesLocation) - file_dialog.setDirectory(movies_location) - if file_dialog.exec() == QDialog.Accepted: - url = file_dialog.selectedUrls()[0] - self._playlist.append(url) - self._playlist_index = len(self._playlist) - 1 - self._player.setSource(url) - self._player.play() + fileDialog = QFileDialog(self) + fileDialog.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen) + fileDialog.setWindowTitle("Open Files") + fileDialog.setMimeTypeFilters(getSupportedMimeTypes()) + fileDialog.selectMimeTypeFilter(MP4) + movieDirs = QStandardPaths.standardLocations(QStandardPaths.StandardLocation.MoviesLocation) + fileDialog.setDirectory(movieDirs[0] if movieDirs else QDir.homePath()) + if fileDialog.exec() == QDialog.DialogCode.Accepted: + self.openUrl(fileDialog.selectedUrls()[0]) + + def openUrl(self, url): + self.m_player.setSource(url) + + @Slot("qlonglong") + def durationChanged(self, duration): + self.m_duration = duration / 1000 + self.m_slider.setMaximum(duration) + + @Slot("qlonglong") + def positionChanged(self, progress): + if not self.m_slider.isSliderDown(): + self.m_slider.setValue(progress) + self.updateDurationInfo(progress / 1000) @Slot() - def _ensure_stopped(self): - if self._player.playbackState() != QMediaPlayer.StoppedState: - self._player.stop() + def metaDataChanged(self): + metaData = self.m_player.metaData() + artist = metaData.value(QMediaMetaData.Key.AlbumArtist) + title = metaData.value(QMediaMetaData.Key.Title) + trackInfo = QApplication.applicationName() + if artist and title: + trackInfo = f"{artist} - {title}" + elif artist: + trackInfo = artist + elif title: + trackInfo = title + self.setTrackInfo(trackInfo) + + for i in range(0, QMediaMetaData.NumMetaData): + field = self.m_metaDataFields[i] + if isinstance(field, QLineEdit): + field.clear() + elif isinstance(field, QLabel): + field.clear() + self.m_metaDataFields[i].setDisabled(True) + self.m_metaDataLabels[i].setDisabled(True) + + for key in metaData.keys(): + i = key.value + field = self.m_metaDataFields[i] + if key == QMediaMetaData.Key.CoverArtImage or key == QMediaMetaData.Key.ThumbnailImage: + if isinstance(field, QLabel): + field.setPixmap(QPixmap.fromImage(metaData.value(key))) + elif isinstance(field, QLineEdit): + field.setText(metaData.stringValue(key)) + + self.m_metaDataFields[i].setDisabled(False) + self.m_metaDataLabels[i].setDisabled(False) + + tracks = self.m_player.videoTracks() + currentVideoTrack = self.m_player.activeVideoTrack() + if currentVideoTrack >= 0 and currentVideoTrack < len(tracks): + track = tracks[currentVideoTrack] + trackKeys = track.keys() + for key in trackKeys: + i = key.value + field = self.m_metaDataFields[i] + if isinstance(field, QLineEdit): + stringValue = track.stringValue(key) + field.setText(stringValue) + self.m_metaDataFields[i].setDisabled(True) + self.m_metaDataLabels[i].setDisabled(True) + + def trackName(self, metaData, index): + name = "" + title = metaData.stringValue(QMediaMetaData.Key.Title) + lang = metaData.value(QMediaMetaData.Key.Language) + if not title: + if lang == QLocale.Language.AnyLanguage: + name = f"Track {index + 1}" + else: + name = QLocale.languageToString(lang) + else: + if lang == QLocale.Language.AnyLanguage: + name = title + else: + langName = QLocale.languageToString(lang) + name = f"{title} - [{langName}]" + return name @Slot() - def previous_clicked(self): - # Go to previous track if we are within the first 5 seconds of playback - # Otherwise, seek to the beginning. - if self._player.position() <= 5000 and self._playlist_index > 0: - self._playlist_index -= 1 - self._playlist.previous() - self._player.setSource(self._playlist[self._playlist_index]) + def tracksChanged(self): + self.m_audioTracks.clear() + self.m_videoTracks.clear() + self.m_subtitleTracks.clear() + + audioTracks = self.m_player.audioTracks() + self.m_audioTracks.addItem("No audio", -1) + for i in range(0, len(audioTracks)): + self.m_audioTracks.addItem(self.trackName(audioTracks[i], i), i) + self.m_audioTracks.setCurrentIndex(self.m_player.activeAudioTrack() + 1) + + videoTracks = self.m_player.videoTracks() + self.m_videoTracks.addItem("No video", -1) + for i in range(0, len(videoTracks)): + self.m_videoTracks.addItem(self.trackName(videoTracks[i], i), i) + self.m_videoTracks.setCurrentIndex(self.m_player.activeVideoTrack() + 1) + + self.m_subtitleTracks.addItem("No subtitles", -1) + subtitleTracks = self.m_player.subtitleTracks() + for i in range(0, len(subtitleTracks)): + self.m_subtitleTracks.addItem(self.trackName(subtitleTracks[i], i), i) + self.m_subtitleTracks.setCurrentIndex(self.m_player.activeSubtitleTrack() + 1) + + @Slot() + def previousClicked(self): + self.m_player.setPosition(0) + + @Slot(int) + def seek(self, mseconds): + self.m_player.setPosition(mseconds) + + @Slot(QMediaPlayer.MediaStatus) + def statusChanged(self, status): + self.handleCursor(status) + # handle status message + if (status == QMediaPlayer.MediaStatus.NoMedia + or status == QMediaPlayer.MediaStatus.LoadedMedia): + self.setStatusInfo("") + elif status == QMediaPlayer.MediaStatus.LoadingMedia: + self.setStatusInfo("Loading...") + elif (status == QMediaPlayer.MediaStatus.BufferingMedia + or status == QMediaPlayer.MediaStatus.BufferedMedia): + progress = round(self.m_player.bufferProgress() * 100.0) + self.setStatusInfo(f"Buffering {progress}%") + elif status == QMediaPlayer.MediaStatus.StalledMedia: + progress = round(self.m_player.bufferProgress() * 100.0) + self.setStatusInfo(f"Stalled {progress}%") + elif status == QMediaPlayer.MediaStatus.EndOfMedia: + QApplication.alert(self) + elif status == QMediaPlayer.MediaStatus.InvalidMedia: + self.displayErrorMessage() + + def handleCursor(self, status): + if (status == QMediaPlayer.MediaStatus.LoadingMedia + or status == QMediaPlayer.MediaStatus.BufferingMedia + or status == QMediaPlayer.MediaStatus.StalledMedia): + self.setCursor(QCursor(Qt.CursorShape.BusyCursor)) + else: + self.unsetCursor() + + @Slot("float") + def bufferingProgress(self, progressV): + progress = round(progressV * 100.0) + if self.m_player.mediaStatus() == QMediaPlayer.MediaStatus.StalledMedia: + self.setStatusInfo(f"Stalled {progress}%") else: - self._player.setPosition(0) + self.setStatusInfo(f"Buffering {progress}%") + + @Slot(bool) + def videoAvailableChanged(self, available): + if not available: + self.m_fullScreenButton.clicked.disconnect(self.m_videoWidget.switchToFullScreen) + self.m_videoWidget.fullScreenChanged.disconnect(self.m_fullScreenButton.setChecked) + self.m_videoWidget.setFullScreen(False) + else: + self.m_fullScreenButton.clicked.connect(self.m_videoWidget.switchToFullScreen) + self.m_videoWidget.fullScreenChanged.connect(self.m_fullScreenButton.setChecked) + if self.m_fullScreenButton.isChecked(): + self.m_videoWidget.setFullScreen(True) + + @Slot() + def selectAudioStream(self): + stream = self.m_audioTracks.currentData() + self.m_player.setActiveAudioTrack(stream) + + @Slot() + def selectVideoStream(self): + stream = self.m_videoTracks.currentData() + self.m_player.setActiveVideoTrack(stream) @Slot() - def next_clicked(self): - if self._playlist_index < len(self._playlist) - 1: - self._playlist_index += 1 - self._player.setSource(self._playlist[self._playlist_index]) - - @Slot("QMediaPlayer::PlaybackState") - def update_buttons(self, state): - media_count = len(self._playlist) - self._play_action.setEnabled(media_count > 0 and state != QMediaPlayer.PlayingState) - self._pause_action.setEnabled(state == QMediaPlayer.PlayingState) - self._stop_action.setEnabled(state != QMediaPlayer.StoppedState) - self._previous_action.setEnabled(self._player.position() > 0) - self._next_action.setEnabled(media_count > 1) - - def show_status_message(self, message): - self.statusBar().showMessage(message, 5000) - - @Slot("QMediaPlayer::Error", str) - def _player_error(self, error, error_string): - print(error_string, file=sys.stderr) - self.show_status_message(error_string) + def selectSubtitleStream(self): + stream = self.m_subtitleTracks.currentData() + self.m_player.setActiveSubtitleTrack(stream) + + def setTrackInfo(self, info): + self.m_trackInfo = info + title = self.m_trackInfo + if self.m_statusInfo: + title += f" | {self.m_statusInfo}" + self.setWindowTitle(title) + + def setStatusInfo(self, info): + self.m_statusInfo = info + title = self.m_trackInfo + if self.m_statusInfo: + title += f" | {self.m_statusInfo}" + self.setWindowTitle(title) @Slot() - def setVolume(self): - self.volumeValue = QAudio.convertVolume(self._volume_slider.value() / 100.0, - QAudio.VolumeScale.LogarithmicVolumeScale, - QAudio.VolumeScale.LinearVolumeScale) - self._audio_output.setVolume(self.volumeValue) - - -if __name__ == '__main__': - app = QApplication(sys.argv) - main_win = MainWindow() - available_geometry = main_win.screen().availableGeometry() - main_win.resize(available_geometry.width() / 3, - available_geometry.height() / 2) - main_win.show() - sys.exit(app.exec()) + def displayErrorMessage(self): + if self.m_player.error() != QMediaPlayer.Error.NoError: + self.setStatusInfo(self.m_player.errorString()) + + def updateDurationInfo(self, currentInfo): + tStr = "" + if currentInfo or self.m_duration: + currentTime = QTime((currentInfo / 3600) % 60, (currentInfo / 60) % 60, + currentInfo % 60, (currentInfo * 1000) % 1000) + totalTime = QTime((self.m_duration / 3600) % 60, (self.m_duration / 60) % 60, + self.m_duration % 60, (self.m_duration * 1000) % 1000) + format = "hh:mm:ss" if self.m_duration > 3600 else "mm:ss" + tStr = currentTime.toString(format) + " / " + totalTime.toString(format) + self.m_labelDuration.setText(tStr) + + @Slot() + def updateAudioDevices(self): + self.m_audioOutputCombo.clear() + + self.m_audioOutputCombo.addItem("Default", QAudioDevice()) + for deviceInfo in QMediaDevices.audioOutputs(): + self.m_audioOutputCombo.addItem(deviceInfo.description(), deviceInfo) + + @Slot(int) + def audioOutputChanged(self, index): + device = self.m_audioOutputCombo.itemData(index) + self.m_player.audioOutput().setDevice(device) diff --git a/examples/multimedia/player/player.pyproject b/examples/multimedia/player/player.pyproject index 2e16f450..cb278248 100644 --- a/examples/multimedia/player/player.pyproject +++ b/examples/multimedia/player/player.pyproject @@ -1,3 +1,7 @@ { - "files": ["player.py"] + "files": ["main.py", + "audiolevelmeter.py", + "player.py", + "playercontrols.py", + "videowidget.py"] } diff --git a/examples/multimedia/player/playercontrols.py b/examples/multimedia/player/playercontrols.py new file mode 100644 index 00000000..2093e99c --- /dev/null +++ b/examples/multimedia/player/playercontrols.py @@ -0,0 +1,162 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtMultimedia import QMediaPlayer, QtAudio +from PySide6.QtWidgets import (QComboBox, QHBoxLayout, QSizePolicy, QSlider, QStyle, + QToolButton, QWidget) +from PySide6.QtGui import QPalette +from PySide6.QtCore import qFuzzyCompare, Qt, Signal, Slot + + +class PlayerControls(QWidget): + + play = Signal() + pause = Signal() + stop = Signal() + previous = Signal() + changeVolume = Signal(float) + changeMuting = Signal(bool) + changeRate = Signal(float) + + def __init__(self, parent=None): + super().__init__(parent) + + style = self.style() + self.m_playerState = QMediaPlayer.PlaybackState.StoppedState + self.m_playerMuted = False + + self.m_playButton = QToolButton(self) + self.m_playButton.setIcon(style.standardIcon(QStyle.StandardPixmap.SP_MediaPlay)) + self.m_playButton.setToolTip("Play") + self.m_playButton.clicked.connect(self.playClicked) + + self.m_pauseButton = QToolButton(self) + self.m_pauseButton.setIcon(style.standardIcon(QStyle.StandardPixmap.SP_MediaPause)) + self.m_pauseButton.setToolTip("Pause") + self.m_pauseButton.clicked.connect(self.pauseClicked) + + self.m_stopButton = QToolButton(self) + self.m_stopButton.setIcon(style.standardIcon(QStyle.StandardPixmap.SP_MediaStop)) + self.m_stopButton.setToolTip("Stop") + self.m_stopButton.clicked.connect(self.stop) + + self.m_previousButton = QToolButton(self) + self.m_previousButton.setIcon(style.standardIcon(QStyle.StandardPixmap.SP_MediaSkipBackward)) # noqa: E501 + self.m_previousButton.setToolTip("Rewind") + self.m_previousButton.clicked.connect(self.previous) + + self.m_muteButton = QToolButton(self) + self.m_muteButton.setIcon(style.standardIcon(QStyle.StandardPixmap.SP_MediaVolume)) + self.m_muteButton.setToolTip("Mute") + self.m_muteButton.clicked.connect(self.muteClicked) + + self.m_volumeSlider = QSlider(Qt.Orientation.Horizontal, self) + self.m_volumeSlider.setRange(0, 100) + sp = self.m_volumeSlider.sizePolicy() + sp.setHorizontalPolicy(QSizePolicy.Policy.MinimumExpanding) + self.m_volumeSlider.setSizePolicy(sp) + self.m_volumeSlider.valueChanged.connect(self.onVolumeSliderValueChanged) + + self.m_rateBox = QComboBox(self) + self.m_rateBox.setToolTip("Rate") + self.m_rateBox.addItem("0.5x", 0.5) + self.m_rateBox.addItem("1.0x", 1.0) + self.m_rateBox.addItem("2.0x", 2.0) + self.m_rateBox.setCurrentIndex(1) + + self.m_rateBox.activated.connect(self.updateRate) + + self._doSetState(QMediaPlayer.PlaybackState.StoppedState, True) + + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.m_stopButton) + layout.addWidget(self.m_previousButton) + layout.addWidget(self.m_pauseButton) + layout.addWidget(self.m_playButton) + layout.addWidget(self.m_muteButton) + layout.addWidget(self.m_volumeSlider) + layout.addWidget(self.m_rateBox) + + def state(self): + return self.m_playerState + + @Slot(QMediaPlayer.PlaybackState) + def setState(self, state): + self._doSetState(state, False) + + def _doSetState(self, state, force): + if state != self.m_playerState or force: + self.m_playerState = state + + baseColor = self.palette().color(QPalette.ColorRole.Base) + inactiveStyleSheet = f"background-color: {baseColor.name()}" + defaultStyleSheet = "" + + if state == QMediaPlayer.PlaybackState.StoppedState: + self.m_stopButton.setStyleSheet(inactiveStyleSheet) + self.m_playButton.setStyleSheet(defaultStyleSheet) + self.m_pauseButton.setStyleSheet(defaultStyleSheet) + elif state == QMediaPlayer.PlaybackState.PlayingState: + self.m_stopButton.setStyleSheet(defaultStyleSheet) + self.m_playButton.setStyleSheet(inactiveStyleSheet) + self.m_pauseButton.setStyleSheet(defaultStyleSheet) + elif state == QMediaPlayer.PlaybackState.PausedState: + self.m_stopButton.setStyleSheet(defaultStyleSheet) + self.m_playButton.setStyleSheet(defaultStyleSheet) + self.m_pauseButton.setStyleSheet(inactiveStyleSheet) + + def volume(self): + linearVolume = QtAudio.convertVolume(self.m_volumeSlider.value() / 100.0, + QtAudio.VolumeScale.LogarithmicVolumeScale, + QtAudio.VolumeScale.LinearVolumeScale) + return linearVolume + + @Slot("float") + def setVolume(self, volume): + logarithmicVolume = QtAudio.convertVolume(volume, QtAudio.VolumeScale.LinearVolumeScale, + QtAudio.VolumeScale.LogarithmicVolumeScale) + self.m_volumeSlider.setValue(round(logarithmicVolume * 100.0)) + + def isMuted(self): + return self.m_playerMuted + + @Slot(bool) + def setMuted(self, muted): + if muted != self.m_playerMuted: + self.m_playerMuted = muted + sp = (QStyle.StandardPixmap.SP_MediaVolumeMuted + if muted else QStyle.StandardPixmap.SP_MediaVolume) + self.m_muteButton.setIcon(self.style().standardIcon(sp)) + + @Slot() + def playClicked(self): + self.play.emit() + + @Slot() + def pauseClicked(self): + self.pause.emit() + + @Slot() + def muteClicked(self): + self.changeMuting.emit(not self.m_playerMuted) + + def playbackRate(self): + return self.m_rateBox.itemData(self.m_rateBox.currentIndex()) + + def setPlaybackRate(self, rate): + for i in range(0, self.m_rateBox.count()): + if qFuzzyCompare(rate, self.m_rateBox.itemData(i)): + self.m_rateBox.setCurrentIndex(i) + return + + self.m_rateBox.addItem(f"{rate}x", rate) + self.m_rateBox.setCurrentIndex(self.m_rateBox.count() - 1) + + @Slot() + def updateRate(self): + self.changeRate.emit(self.playbackRate()) + + @Slot() + def onVolumeSliderValueChanged(self): + self.changeVolume.emit(self.volume()) diff --git a/examples/multimedia/player/videowidget.py b/examples/multimedia/player/videowidget.py new file mode 100644 index 00000000..d2ec9c7f --- /dev/null +++ b/examples/multimedia/player/videowidget.py @@ -0,0 +1,41 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtMultimediaWidgets import QVideoWidget +from PySide6.QtWidgets import QSizePolicy +from PySide6.QtGui import QPalette +from PySide6.QtCore import Qt, QOperatingSystemVersion, Slot + + +class VideoWidget(QVideoWidget): + + def __init__(self, parent=None): + super().__init__(parent) + self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored) + p = self.palette() + p.setColor(QPalette.ColorRole.Window, Qt.GlobalColor.black) + self.setPalette(p) + if QOperatingSystemVersion.currentType() != QOperatingSystemVersion.OSType.Android: + self.setAttribute(Qt.WidgetAttribute.WA_OpaquePaintEvent) + + def keyPressEvent(self, event): + key = event.key() + if (key == Qt.Key.Key_Escape or key == Qt.Key.Key_Back) and self.isFullScreen(): + self.setFullScreen(False) + event.accept() + elif key == Qt.Key.Key_Enter and event.modifiers() & Qt.Key.Key_Alt: + self.setFullScreen(not self.isFullScreen()) + event.accept() + else: + super().keyPressEvent(event) + + @Slot() + def switchToFullScreen(self): + self.setFullScreen(True) + + def mouseDoubleClickEvent(self, event): + self.setFullScreen(not self.isFullScreen()) + event.accept() + + def mousePressEvent(self, event): + super().mousePressEvent(event) diff --git a/examples/pdfwidgets/pdfviewer/images/go-down-search.svgz b/examples/pdfwidgets/pdfviewer/images/go-down-search.svgz new file mode 100644 index 00000000..f845473e Binary files /dev/null and b/examples/pdfwidgets/pdfviewer/images/go-down-search.svgz differ diff --git a/examples/pdfwidgets/pdfviewer/images/go-up-search.svgz b/examples/pdfwidgets/pdfviewer/images/go-up-search.svgz new file mode 100644 index 00000000..6378721f Binary files /dev/null and b/examples/pdfwidgets/pdfviewer/images/go-up-search.svgz differ diff --git a/examples/pdfwidgets/pdfviewer/mainwindow.py b/examples/pdfwidgets/pdfviewer/mainwindow.py index 154c8770..f6344195 100644 --- a/examples/pdfwidgets/pdfviewer/mainwindow.py +++ b/examples/pdfwidgets/pdfviewer/mainwindow.py @@ -5,13 +5,15 @@ from __future__ import annotations import math import sys -from PySide6.QtPdf import QPdfBookmarkModel, QPdfDocument +from PySide6.QtPdf import QPdfBookmarkModel, QPdfDocument, QPdfSearchModel from PySide6.QtPdfWidgets import QPdfView -from PySide6.QtWidgets import (QDialog, QFileDialog, QMainWindow, QMessageBox, +from PySide6.QtWidgets import (QDialog, QFileDialog, QLineEdit, QMainWindow, QMessageBox, QSpinBox) -from PySide6.QtCore import QModelIndex, QPoint, QStandardPaths, QUrl, Slot +from PySide6.QtGui import QKeySequence, QShortcut +from PySide6.QtCore import QModelIndex, QPoint, QPointF, QStandardPaths, QUrl, Qt, Slot from zoomselector import ZoomSelector +from searchresultdelegate import SearchResultDelegate from ui_mainwindow import Ui_MainWindow @@ -50,12 +52,47 @@ class MainWindow(QMainWindow): self.ui.bookmarkView.setModel(bookmark_model) self.ui.bookmarkView.activated.connect(self.bookmark_selected) - self.ui.tabWidget.setTabEnabled(1, False) # disable 'Pages' tab for now + self.ui.thumbnailsView.setModel(self.m_document.pageModel()) self.ui.pdfView.setDocument(self.m_document) self.ui.pdfView.zoomFactorChanged.connect(self.m_zoomSelector.set_zoom_factor) + self.m_searchModel = QPdfSearchModel(self) + self.m_searchModel.setDocument(self.m_document) + self.m_searchField = QLineEdit(self) + + self.ui.pdfView.setSearchModel(self.m_searchModel) + self.ui.searchToolBar.insertWidget(self.ui.actionFindPrevious, self.m_searchField) + self.m_findShortcut = QShortcut(QKeySequence.StandardKey.Find, self) + self.m_findShortcut.activated.connect(self.setSearchFocus) + self.m_searchField.setPlaceholderText("Find in document") + self.m_searchField.setMaximumWidth(400) + self.m_searchField.textEdited.connect(self.searchTextChanged) + self.ui.searchResultsView.setModel(self.m_searchModel) + self.m_delegate = SearchResultDelegate(self) + self.ui.searchResultsView.setItemDelegate(self.m_delegate) + sel_model = self.ui.searchResultsView.selectionModel() + sel_model.currentChanged.connect(self.searchResultSelected) + + @Slot() + def setSearchFocus(self): + self.m_searchField.setFocus(Qt.FocusReason.ShortcutFocusReason) + + @Slot() + def searchTextChanged(self, text): + self.m_searchModel.setSearchString(text) + self.ui.tabWidget.setCurrentWidget(self.ui.searchResultsTab) + + @Slot(QModelIndex, QModelIndex) + def searchResultSelected(self, current, previous): + if not current.isValid(): + return + page = current.data(QPdfSearchModel.Role.Page.value) + location = current.data(QPdfSearchModel.Role.Location.value) + self.ui.pdfView.pageNavigator().jump(page, location) + self.ui.pdfView.setCurrentSearchResultIndex(current.row()) + @Slot(QUrl) def open(self, doc_location): if doc_location.isLocalFile(): @@ -94,6 +131,20 @@ class MainWindow(QMainWindow): if to_open.isValid(): self.open(to_open) + @Slot() + def on_actionFindNext_triggered(self): + next = self.ui.searchResultsView.currentIndex().row() + 1 + if next >= self.m_searchModel.rowCount(QModelIndex()): + next = 0 + self.ui.searchResultsView.setCurrentIndex(self.m_searchModel.index(next)) + + @Slot() + def on_actionFindPrevious_triggered(self): + prev = self.ui.searchResultsView.currentIndex().row() - 1 + if prev < 0: + prev = self.m_searchModel.rowCount(QModelIndex()) - 1 + self.ui.searchResultsView.setCurrentIndex(self.m_searchModel.index(prev)) + @Slot() def on_actionQuit_triggered(self): self.close() @@ -127,6 +178,11 @@ class MainWindow(QMainWindow): nav = self.ui.pdfView.pageNavigator() nav.jump(nav.currentPage() + 1, QPoint(), nav.currentZoom()) + @Slot(QModelIndex) + def on_thumbnailsView_activated(self, index): + nav = self.ui.pdfView.pageNavigator() + nav.jump(index.row(), QPointF(), nav.currentZoom()) + @Slot() def on_actionContinuous_triggered(self): cont_checked = self.ui.actionContinuous.isChecked() diff --git a/examples/pdfwidgets/pdfviewer/mainwindow.ui b/examples/pdfwidgets/pdfviewer/mainwindow.ui index 3bf46887..a9a153bf 100644 --- a/examples/pdfwidgets/pdfviewer/mainwindow.ui +++ b/examples/pdfwidgets/pdfviewer/mainwindow.ui @@ -111,6 +111,74 @@ Pages + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + 0 + 0 + + + + + 128 + 128 + + + + QListView::Movement::Static + + + QListView::ResizeMode::Adjust + + + QListView::ViewMode::IconMode + + + + + + + + Search Results + + + + 0 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + + @@ -188,6 +256,19 @@ + + + toolBar + + + TopToolBarArea + + + false + + + + @@ -233,7 +314,7 @@ Zoom In - Ctrl++ + Ctrl+= @@ -310,6 +391,36 @@ forward to next view + + + + :/icons/images/go-down-search.svgz:/icons/images/go-down-search.svgz + + + Find Next + + + Find the next occurrence of the phrase + + + F3 + + + + + + :/icons/images/go-up-search.svgz:/icons/images/go-up-search.svgz + + + Find Previous + + + Find the previous occurrence of the phrase + + + Shift+F3 + + diff --git a/examples/pdfwidgets/pdfviewer/pdfviewer.pyproject b/examples/pdfwidgets/pdfviewer/pdfviewer.pyproject index cbd5f156..ecaae7c3 100644 --- a/examples/pdfwidgets/pdfviewer/pdfviewer.pyproject +++ b/examples/pdfwidgets/pdfviewer/pdfviewer.pyproject @@ -1,4 +1,4 @@ { - "files": ["main.py", "mainwindow.py", "zoomselector.py", + "files": ["main.py", "mainwindow.py", "zoomselector.py", "searchresultdelegate.py", "mainwindow.ui","resources.qrc"] } diff --git a/examples/pdfwidgets/pdfviewer/rc_resources.py b/examples/pdfwidgets/pdfviewer/rc_resources.py index 7e386e99..d0daa316 100644 --- a/examples/pdfwidgets/pdfviewer/rc_resources.py +++ b/examples/pdfwidgets/pdfviewer/rc_resources.py @@ -1,11 +1,29 @@ # Resource object code (Python 3) # Created by: object code -# Created by: The Resource Compiler for Qt version 6.4.0 +# Created by: The Resource Compiler for Qt version 6.10.0 # WARNING! All changes made in this file will be lost! from PySide6 import QtCore qt_resource_data = b"\ +\x00\x00\x00\xf1\ +\x1f\ +\x8b\x08\x08A0\x10d\x00\x03go-up-s\ +earch.svg\x00]OA\x8e\xc20\ +\x0c\xbc\xf7\x15\x969'i\xd3\x02\x85m{\xe1\x0a\xa7\ +\xdd\x0f\xa0\x90m*\x05Z5\x86\x80V\xfbw\x92\x82\ +z`,K#\x8fF3\xae\xdc\xad\x05\xa3\xbb\xd6P\ +\x8d\xb2@\xf0\xdd\x89\xcc\x8b\xde\xcf\xf6\xe2j4D\xc3\ +V\x08\xef=\xf79\xef\xc7V\xc84ME0b\x93\ +@@\xe5\xe8a5\xd0c\xd05\x92\xbe\x93P\xce!\ +t\xa7\x1a\xd5u\x1c\xf5\x85\x98\xeam?2\xa7\x8c>\ +\xeb\xb7+\x82\xef\xe2\xfd{:\xb3\x9f\xe0\x84\xbfY\x8b\ +\x98l\xdb\x85\xcc\xe5Jn\xbef\xe9\xff\x15+\xa6\xdc\ +w\x87\xe1H\x06B\xe2\xa1\xe0\xebt\x0d\xd9j_@\ +\xb6\xe4r\x93\xdb\x92\x95\x10\x86E!\xee>\x93PF\ +\x82\xa0\xec\xd1\x85\x17?k \xfcv\xd6\xce\xf5'\x19\ +E\x93T\xf1\xeb&y\x02\x19\x0e\x0c\xf45\x01\x00\x00\ +\ \x00\x001G\ \x1f\ \x8b\x08\x00\x00\x00\x00\x00\x00\x00\xec}\xebs\x1b\xc7\xb1\ @@ -1609,6 +1627,29 @@ U\xda\x18\xd8\xccs\x13\x15\x87\xa4`-\x83\x1eT\xcd\ \xe0i\x8a\xa6\xd0\x09\x9f4\xdd\xda'm\xd6'\xbf\xdb\ \xbf\xfa\xf1\xdd\x9f~\xf7_\xea_?\x7f\x00\x9a\x00\x00\ \ +\x00\x00\x01J\ +\x1f\ +\x8b\x08\x08I0\x10d\x00\x03go-down\ +-search.svg\x00]Q\xcbn\ +\xc20\x10\xbc\xf3\x15+\xf7\x1c?\xf3&\xe1\xd0^\xe9\ +\xa9\xfd\x01\x14\x0c\x89\x1a\xe2(v\x09i\xd5\x7f\xafm\ +\x5cT\x90,ywvfvdW\xfa|\x84\xcb\xa9\ +\x1ft\x8dZc\xc6\x92\x90y\x9e\xf1,\xb0\x9a\x8e\x84\ +SJ\x89e 8wr~V\x97\x1aQ\xa0\xc0c\ +{\xd0f\x05P\xed\xe5AC\xb7\xaf\x91+\x04M\x98\ +\x87\xed@\x9b\xa5\x97`\x96Q\xd6\xc8\xc8\x8b!\x8d\xd6\ +\xc83\x9b\xcfi\x92\x83\x89\x1a\xd5\xab)\xd2M+O\ +2\xa8\x00\xf0\x8bC\xdf<\x18\xbd[\x1d|\x87\x09\x80\ +\x17\x94O\x5c\xf0\x94\x17\xeb\x00\xff\x84\xbb\x22~\xa3\x0f\ +E\x5c\x18[U\xe3\xce\xb4\xe0\xf1\x1a\x1d\xba\xbe/\xc3\ +n\xbfd\xed\x90H\x8d\xbb\xa63K\xc9\xd6\xdaL\xea\ +C\x96\x83\x1a$\xba\x9a\xda\xb0\xaf\x10\xe3\x8cfT0\ +\x0e9l!\x86\xfc\xd6o\x81Q\xccx\x02,\xc6\xb9\ +\xe0\x16s\x10\x07\x96\x06\x8ak\x05\xce\xb3{\x06\xa7\xf7\ +\x1e\x05\xe6\x05/\xd2\xc2\xfb[>K3\xd7Y\x89\xf3\ +\xbe:&\x7f\x1c\xbf3X\xdd\x18\xff#~A\x08\xdf\ +\xf4;m?\xf5\xf1=\xc3\x94lV\x95\xfb\xd9\xcd\xea\ +\x17i\x1a\x96c\x02\x02\x00\x00\ \x00\x00\x15,\ \x1f\ \x8b\x08\x00\x00\x00\x00\x00\x00\x00\xed=ko\xe3F\x92\ @@ -3401,6 +3442,11 @@ qt_resource_name = b"\ \x07\x03}\xc3\ \x00i\ \x00m\x00a\x00g\x00e\x00s\ +\x00\x11\ +\x0e\x9eN\xea\ +\x00g\ +\x00o\x00-\x00u\x00p\x00-\x00s\x00e\x00a\x00r\x00c\x00h\x00.\x00s\x00v\x00g\x00z\ +\ \x00\x16\ \x02\x1b\xe1\x0a\ \x00g\ @@ -3416,6 +3462,11 @@ qt_resource_name = b"\ \x00g\ \x00o\x00-\x00p\x00r\x00e\x00v\x00i\x00o\x00u\x00s\x00-\x00v\x00i\x00e\x00w\x00.\ \x00s\x00v\x00g\x00z\ +\x00\x13\ +\x03\xa8\x05\x0a\ +\x00g\ +\x00o\x00-\x00d\x00o\x00w\x00n\x00-\x00s\x00e\x00a\x00r\x00c\x00h\x00.\x00s\x00v\ +\x00g\x00z\ \x00\x0d\ \x0e\xb9\xa6*\ \x00z\ @@ -3441,22 +3492,26 @@ qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x10\x00\x02\x00\x00\x00\x07\x00\x00\x00\x03\ +\x00\x00\x00\x10\x00\x02\x00\x00\x00\x09\x00\x00\x00\x03\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x010\x00\x00\x00\x00\x00\x01\x00\x00\xba\xe2\ -\x00\x00\x01\x81\x8a\xd9\xf0\x94\ -\x00\x00\x00|\x00\x00\x00\x00\x00\x01\x00\x00J'\ -\x00\x00\x01\x81\x8a\xd9\xf0\x94\ -\x00\x00\x00\xf6\x00\x00\x00\x00\x00\x01\x00\x00\x89\xa4\ -\x00\x00\x01\x81\x8a\xd9\xf0\x94\ +\x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x00\xbd%\ +\x00\x00\x01\x975l\xc7\xe5\ +\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x01\x00\x00K\x1c\ +\x00\x00\x01\x975l\xc7\xe5\ +\x00\x00\x01J\x00\x00\x00\x00\x00\x01\x00\x00\x8b\xe7\ +\x00\x00\x01\x975l\xc7\xe5\ +\x00\x00\x00J\x00\x00\x00\x00\x00\x01\x00\x00\x00\xf5\ +\x00\x00\x01\x975l\xc7\xe5\ +\x00\x00\x00\xd4\x00\x00\x00\x00\x00\x01\x00\x00d\xb1\ +\x00\x00\x01\x99v\xc8\x0b%\ +\x00\x00\x00|\x00\x00\x00\x00\x00\x01\x00\x002@\ +\x00\x00\x01\x975l\xc7\xe5\ +\x00\x00\x01 \x00\x00\x00\x00\x00\x01\x00\x00{/\ +\x00\x00\x01\x975l\xc7\xe5\ \x00\x00\x00\x22\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x81\x8a\xd9\xf0\x94\ -\x00\x00\x00T\x00\x00\x00\x00\x00\x01\x00\x001K\ -\x00\x00\x01\x81\x8a\xd9\xf0\x94\ -\x00\x00\x00\xcc\x00\x00\x00\x00\x00\x01\x00\x00x\xec\ -\x00\x00\x01\x81\x8a\xd9\xf0\x94\ -\x00\x00\x00\xac\x00\x00\x00\x00\x00\x01\x00\x00c\xbc\ -\x00\x00\x01\x81\x8a\xd9\xf0\x94\ +\x00\x00\x01\x99v\xc7\xf9e\ +\x00\x00\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00e\xff\ +\x00\x00\x01\x975l\xc7\xe6\ " def qInitResources(): diff --git a/examples/pdfwidgets/pdfviewer/resources.qrc b/examples/pdfwidgets/pdfviewer/resources.qrc index db77763d..ea408b82 100644 --- a/examples/pdfwidgets/pdfviewer/resources.qrc +++ b/examples/pdfwidgets/pdfviewer/resources.qrc @@ -1,10 +1,12 @@ images/document-open.svgz + images/go-down-search.svgz images/go-next-view.svgz images/go-previous-view.svgz images/go-next-view-page.svgz images/go-previous-view-page.svgz + images/go-up-search.svgz images/zoom-in.svgz images/zoom-out.svgz diff --git a/examples/pdfwidgets/pdfviewer/searchresultdelegate.py b/examples/pdfwidgets/pdfviewer/searchresultdelegate.py new file mode 100644 index 00000000..72c39daa --- /dev/null +++ b/examples/pdfwidgets/pdfviewer/searchresultdelegate.py @@ -0,0 +1,47 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import Qt +from PySide6.QtGui import QFont, QFontMetrics +from PySide6.QtWidgets import QStyle, QStyledItemDelegate +from PySide6.QtPdf import QPdfSearchModel + + +class SearchResultDelegate(QStyledItemDelegate): + + def __init__(self, parent=None): + super().__init__(parent) + + def paint(self, painter, option, index): + displayText = index.data() + boldBegin = displayText.find("") + 3 + boldEnd = displayText.find("", boldBegin) + if boldBegin >= 3 and boldEnd > boldBegin: + page = index.data(QPdfSearchModel.Role.Page.value) + pageLabel = f"Page {page}: " + boldText = displayText[boldBegin:boldEnd] + if option.state & QStyle.State_Selected: + painter.fillRect(option.rect, option.palette.highlight()) + defaultFont = painter.font() + fm = painter.fontMetrics() + pageLabelWidth = fm.horizontalAdvance(pageLabel) + yOffset = (option.rect.height() - fm.height()) / 2 + fm.ascent() + painter.drawText(0, option.rect.y() + yOffset, pageLabel) + boldFont = QFont(defaultFont) + boldFont.setBold(True) + boldWidth = QFontMetrics(boldFont).horizontalAdvance(boldText) + prefixSuffixWidth = (option.rect.width() - pageLabelWidth - boldWidth) / 2 + painter.setFont(boldFont) + painter.drawText(pageLabelWidth + prefixSuffixWidth, option.rect.y() + yOffset, + boldText) + painter.setFont(defaultFont) + suffix = fm.elidedText(displayText[boldEnd + 4:], + Qt.TextElideMode.ElideRight, prefixSuffixWidth) + painter.drawText(pageLabelWidth + prefixSuffixWidth + boldWidth, + option.rect.y() + yOffset, suffix) + prefix = fm.elidedText(displayText[0:boldBegin - 3], + Qt.TextElideMode.ElideLeft, prefixSuffixWidth) + painter.drawText(pageLabelWidth + prefixSuffixWidth - fm.horizontalAdvance(prefix), + option.rect.y() + yOffset, prefix) + else: + super().paint(painter, option, index) diff --git a/examples/pdfwidgets/pdfviewer/ui_mainwindow.py b/examples/pdfwidgets/pdfviewer/ui_mainwindow.py index c31da6ff..46c319a7 100644 --- a/examples/pdfwidgets/pdfviewer/ui_mainwindow.py +++ b/examples/pdfwidgets/pdfviewer/ui_mainwindow.py @@ -3,7 +3,7 @@ ################################################################################ ## Form generated from reading UI file 'mainwindow.ui' ## -## Created by: Qt User Interface Compiler version 6.8.0 +## Created by: Qt User Interface Compiler version 6.10.0 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -17,10 +17,10 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, QPainter, QPalette, QPixmap, QRadialGradient, QTransform) from PySide6.QtPdfWidgets import QPdfView -from PySide6.QtWidgets import (QApplication, QHeaderView, QMainWindow, QMenu, - QMenuBar, QSizePolicy, QSplitter, QStatusBar, - QTabWidget, QToolBar, QTreeView, QVBoxLayout, - QWidget) +from PySide6.QtWidgets import (QApplication, QHeaderView, QListView, QMainWindow, + QMenu, QMenuBar, QSizePolicy, QSplitter, + QStatusBar, QTabWidget, QToolBar, QTreeView, + QVBoxLayout, QWidget) import rc_resources class Ui_MainWindow(object): @@ -104,6 +104,26 @@ class Ui_MainWindow(object): icon8 = QIcon() icon8.addFile(u":/icons/images/go-next-view.svgz", QSize(), QIcon.Mode.Normal, QIcon.State.Off) self.actionForward.setIcon(icon8) + self.actionFindNext = QAction(MainWindow) + self.actionFindNext.setObjectName(u"actionFindNext") + icon9 = QIcon() + iconThemeName = u"go-down" + if QIcon.hasThemeIcon(iconThemeName): + icon9 = QIcon.fromTheme(iconThemeName) + else: + icon9.addFile(u":/icons/images/go-down-search.svgz", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + + self.actionFindNext.setIcon(icon9) + self.actionFindPrevious = QAction(MainWindow) + self.actionFindPrevious.setObjectName(u"actionFindPrevious") + icon10 = QIcon() + iconThemeName = u"go-up" + if QIcon.hasThemeIcon(iconThemeName): + icon10 = QIcon.fromTheme(iconThemeName) + else: + icon10.addFile(u":/icons/images/go-up-search.svgz", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + + self.actionFindPrevious.setIcon(icon10) self.centralWidget = QWidget(MainWindow) self.centralWidget.setObjectName(u"centralWidget") self.verticalLayout = QVBoxLayout(self.centralWidget) @@ -148,7 +168,37 @@ class Ui_MainWindow(object): self.tabWidget.addTab(self.bookmarkTab, "") self.pagesTab = QWidget() self.pagesTab.setObjectName(u"pagesTab") + self.verticalLayout_4 = QVBoxLayout(self.pagesTab) + self.verticalLayout_4.setSpacing(6) + self.verticalLayout_4.setContentsMargins(11, 11, 11, 11) + self.verticalLayout_4.setObjectName(u"verticalLayout_4") + self.verticalLayout_4.setContentsMargins(2, 2, 2, 2) + self.thumbnailsView = QListView(self.pagesTab) + self.thumbnailsView.setObjectName(u"thumbnailsView") + sizePolicy.setHeightForWidth(self.thumbnailsView.sizePolicy().hasHeightForWidth()) + self.thumbnailsView.setSizePolicy(sizePolicy) + self.thumbnailsView.setIconSize(QSize(128, 128)) + self.thumbnailsView.setMovement(QListView.Movement.Static) + self.thumbnailsView.setResizeMode(QListView.ResizeMode.Adjust) + self.thumbnailsView.setViewMode(QListView.ViewMode.IconMode) + + self.verticalLayout_4.addWidget(self.thumbnailsView) + self.tabWidget.addTab(self.pagesTab, "") + self.searchResultsTab = QWidget() + self.searchResultsTab.setObjectName(u"searchResultsTab") + self.verticalLayout_5 = QVBoxLayout(self.searchResultsTab) + self.verticalLayout_5.setSpacing(0) + self.verticalLayout_5.setContentsMargins(11, 11, 11, 11) + self.verticalLayout_5.setObjectName(u"verticalLayout_5") + self.verticalLayout_5.setContentsMargins(2, 2, 2, 2) + self.searchResultsView = QListView(self.searchResultsTab) + self.searchResultsView.setObjectName(u"searchResultsView") + self.searchResultsView.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + + self.verticalLayout_5.addWidget(self.searchResultsView) + + self.tabWidget.addTab(self.searchResultsTab, "") self.splitter.addWidget(self.tabWidget) self.pdfView = QPdfView(self.splitter) self.pdfView.setObjectName(u"pdfView") @@ -183,6 +233,9 @@ class Ui_MainWindow(object): self.statusBar = QStatusBar(MainWindow) self.statusBar.setObjectName(u"statusBar") MainWindow.setStatusBar(self.statusBar) + self.searchToolBar = QToolBar(MainWindow) + self.searchToolBar.setObjectName(u"searchToolBar") + MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.searchToolBar) self.menuBar.addAction(self.menuFile.menuAction()) self.menuBar.addAction(self.menuView.menuAction()) @@ -204,6 +257,8 @@ class Ui_MainWindow(object): self.mainToolBar.addSeparator() self.mainToolBar.addAction(self.actionBack) self.mainToolBar.addAction(self.actionForward) + self.searchToolBar.addAction(self.actionFindPrevious) + self.searchToolBar.addAction(self.actionFindNext) self.retranslateUi(MainWindow) @@ -227,7 +282,7 @@ class Ui_MainWindow(object): self.actionAbout_Qt.setText(QCoreApplication.translate("MainWindow", u"About Qt", None)) self.actionZoom_In.setText(QCoreApplication.translate("MainWindow", u"Zoom In", None)) #if QT_CONFIG(shortcut) - self.actionZoom_In.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl++", None)) + self.actionZoom_In.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+=", None)) #endif // QT_CONFIG(shortcut) self.actionZoom_Out.setText(QCoreApplication.translate("MainWindow", u"Zoom Out", None)) #if QT_CONFIG(shortcut) @@ -250,10 +305,26 @@ class Ui_MainWindow(object): #if QT_CONFIG(tooltip) self.actionForward.setToolTip(QCoreApplication.translate("MainWindow", u"forward to next view", None)) #endif // QT_CONFIG(tooltip) + self.actionFindNext.setText(QCoreApplication.translate("MainWindow", u"Find Next", None)) +#if QT_CONFIG(tooltip) + self.actionFindNext.setToolTip(QCoreApplication.translate("MainWindow", u"Find the next occurrence of the phrase", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(shortcut) + self.actionFindNext.setShortcut(QCoreApplication.translate("MainWindow", u"F3", None)) +#endif // QT_CONFIG(shortcut) + self.actionFindPrevious.setText(QCoreApplication.translate("MainWindow", u"Find Previous", None)) +#if QT_CONFIG(tooltip) + self.actionFindPrevious.setToolTip(QCoreApplication.translate("MainWindow", u"Find the previous occurrence of the phrase", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(shortcut) + self.actionFindPrevious.setShortcut(QCoreApplication.translate("MainWindow", u"Shift+F3", None)) +#endif // QT_CONFIG(shortcut) self.tabWidget.setTabText(self.tabWidget.indexOf(self.bookmarkTab), QCoreApplication.translate("MainWindow", u"Bookmarks", None)) self.tabWidget.setTabText(self.tabWidget.indexOf(self.pagesTab), QCoreApplication.translate("MainWindow", u"Pages", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.searchResultsTab), QCoreApplication.translate("MainWindow", u"Search Results", None)) self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"File", None)) self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"Help", None)) self.menuView.setTitle(QCoreApplication.translate("MainWindow", u"View", None)) + self.searchToolBar.setWindowTitle(QCoreApplication.translate("MainWindow", u"toolBar", None)) # retranslateUi diff --git a/examples/qml/tutorials/extending-qml-advanced/advanced4-Grouped-properties/People/Main.qml b/examples/qml/tutorials/extending-qml-advanced/advanced4-Grouped-properties/People/Main.qml index 3c34234f..525c377c 100644 --- a/examples/qml/tutorials/extending-qml-advanced/advanced4-Grouped-properties/People/Main.qml +++ b/examples/qml/tutorials/extending-qml-advanced/advanced4-Grouped-properties/People/Main.qml @@ -1,9 +1,8 @@ // Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -import QtQuick - import People +import QtQuick // For QColor BirthdayParty { host: Boy { @@ -15,7 +14,8 @@ BirthdayParty { name: "Leo Hodges" shoe { size: 10; color: "black"; brand: "Thebok"; price: 59.95 } } - Boy { name: "Jack Smith" + Boy { + name: "Jack Smith" shoe { size: 8 color: "blue" @@ -28,6 +28,6 @@ BirthdayParty { shoe.size: 7 shoe.color: "red" shoe.brand: "Job Macobs" - shoe.price: 699.99 + shoe.price: 99.99 } } diff --git a/examples/qml/tutorials/extending-qml-advanced/advanced5-Attached-properties/People/Main.qml b/examples/qml/tutorials/extending-qml-advanced/advanced5-Attached-properties/People/Main.qml index 795d6386..4fedadbc 100644 --- a/examples/qml/tutorials/extending-qml-advanced/advanced5-Attached-properties/People/Main.qml +++ b/examples/qml/tutorials/extending-qml-advanced/advanced5-Attached-properties/People/Main.qml @@ -2,21 +2,22 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import People +import QtQuick // For QColor BirthdayParty { Boy { name: "Robert Campbell" - BirthdayParty.rsvp: "2009-07-01" + BirthdayParty.rsvp: Date.fromLocaleString(Qt.locale(), "2023-03-01", "yyyy-MM-dd") } Boy { name: "Leo Hodges" - shoe_size: 10 - BirthdayParty.rsvp: "2009-07-06" + shoe { size: 10; color: "black"; brand: "Reebok"; price: 59.95 } + BirthdayParty.rsvp: Date.fromLocaleString(Qt.locale(), "2023-03-03", "yyyy-MM-dd") } host: Boy { name: "Jack Smith" - shoe_size: 8 + shoe { size: 8; color: "blue"; brand: "Puma"; price: 19.95 } } } diff --git a/examples/qml/tutorials/extending-qml-advanced/advanced5-Attached-properties/person.py b/examples/qml/tutorials/extending-qml-advanced/advanced5-Attached-properties/person.py index 8deb7d0b..db3b8d5b 100644 --- a/examples/qml/tutorials/extending-qml-advanced/advanced5-Attached-properties/person.py +++ b/examples/qml/tutorials/extending-qml-advanced/advanced5-Attached-properties/person.py @@ -3,6 +3,7 @@ from __future__ import annotations from PySide6.QtCore import QObject, Property, Signal +from PySide6.QtGui import QColor from PySide6.QtQml import QmlAnonymous, QmlElement # To be used on the @QmlElement decorator @@ -11,15 +12,69 @@ QML_IMPORT_NAME = "People" QML_IMPORT_MAJOR_VERSION = 1 +@QmlAnonymous +class ShoeDescription(QObject): + brand_changed = Signal() + size_changed = Signal() + price_changed = Signal() + color_changed = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self._brand = '' + self._size = 0 + self._price = 0 + self._color = QColor() + + @Property(str, notify=brand_changed, final=True) + def brand(self): + return self._brand + + @brand.setter + def brand(self, b): + if self._brand != b: + self._brand = b + self.brand_changed.emit() + + @Property(int, notify=size_changed, final=True) + def size(self): + return self._size + + @size.setter + def size(self, s): + if self._size != s: + self._size = s + self.size_changed.emit() + + @Property(float, notify=price_changed, final=True) + def price(self): + return self._price + + @price.setter + def price(self, p): + if self._price != p: + self._price = p + self.price_changed.emit() + + @Property(QColor, notify=color_changed, final=True) + def color(self): + return self._color + + @color.setter + def color(self, c): + if self._color != c: + self._color = c + self.color_changed.emit() + + @QmlAnonymous class Person(QObject): name_changed = Signal() - shoe_size_changed = Signal() def __init__(self, parent=None): super().__init__(parent) self._name = '' - self._shoe_size = 0 + self._shoe = ShoeDescription() @Property(str, notify=name_changed, final=True) def name(self): @@ -31,13 +86,9 @@ class Person(QObject): self._name = n self.name_changed.emit() - @Property(int, notify=shoe_size_changed, final=True) - def shoe_size(self): - return self._shoe_size - - @shoe_size.setter - def shoe_size(self, s): - self._shoe_size = s + @Property(ShoeDescription, final=True) + def shoe(self): + return self._shoe @QmlElement diff --git a/examples/qml/tutorials/extending-qml-advanced/advanced6-Property-value-source/People/Main.qml b/examples/qml/tutorials/extending-qml-advanced/advanced6-Property-value-source/People/Main.qml index 254265a8..db0d4613 100644 --- a/examples/qml/tutorials/extending-qml-advanced/advanced6-Property-value-source/People/Main.qml +++ b/examples/qml/tutorials/extending-qml-advanced/advanced6-Property-value-source/People/Main.qml @@ -2,26 +2,37 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import People +import QtQuick // For QColor BirthdayParty { - HappyBirthdaySong on announcement { name: "Bob Jones" } + id: party + HappyBirthdaySong on announcement { + name: party.host.name + } onPartyStarted: (time) => { console.log("This party started rockin' at " + time); } + host: Boy { name: "Bob Jones" - shoe_size: 12 + shoe { size: 12; color: "white"; brand: "Nike"; price: 90.0 } } Boy { name: "Leo Hodges" - BirthdayParty.rsvp: "2009-07-06" + BirthdayParty.rsvp: Date.fromLocaleString(Qt.locale(), "2023-03-01", "yyyy-MM-dd") + shoe { size: 10; color: "black"; brand: "Reebok"; price: 59.95 } } Boy { name: "Jack Smith" + shoe { size: 8; color: "blue"; brand: "Puma"; price: 19.95 } } Girl { name: "Anne Brown" - BirthdayParty.rsvp: "2009-07-01" + BirthdayParty.rsvp: Date.fromLocaleString(Qt.locale(), "2023-03-03", "yyyy-MM-dd") + shoe.size: 7 + shoe.color: "red" + shoe.brand: "Marc Jacobs" + shoe.price: 99.99 } } diff --git a/examples/qml/tutorials/extending-qml-advanced/advanced6-Property-value-source/person.py b/examples/qml/tutorials/extending-qml-advanced/advanced6-Property-value-source/person.py index 8deb7d0b..db3b8d5b 100644 --- a/examples/qml/tutorials/extending-qml-advanced/advanced6-Property-value-source/person.py +++ b/examples/qml/tutorials/extending-qml-advanced/advanced6-Property-value-source/person.py @@ -3,6 +3,7 @@ from __future__ import annotations from PySide6.QtCore import QObject, Property, Signal +from PySide6.QtGui import QColor from PySide6.QtQml import QmlAnonymous, QmlElement # To be used on the @QmlElement decorator @@ -11,15 +12,69 @@ QML_IMPORT_NAME = "People" QML_IMPORT_MAJOR_VERSION = 1 +@QmlAnonymous +class ShoeDescription(QObject): + brand_changed = Signal() + size_changed = Signal() + price_changed = Signal() + color_changed = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self._brand = '' + self._size = 0 + self._price = 0 + self._color = QColor() + + @Property(str, notify=brand_changed, final=True) + def brand(self): + return self._brand + + @brand.setter + def brand(self, b): + if self._brand != b: + self._brand = b + self.brand_changed.emit() + + @Property(int, notify=size_changed, final=True) + def size(self): + return self._size + + @size.setter + def size(self, s): + if self._size != s: + self._size = s + self.size_changed.emit() + + @Property(float, notify=price_changed, final=True) + def price(self): + return self._price + + @price.setter + def price(self, p): + if self._price != p: + self._price = p + self.price_changed.emit() + + @Property(QColor, notify=color_changed, final=True) + def color(self): + return self._color + + @color.setter + def color(self, c): + if self._color != c: + self._color = c + self.color_changed.emit() + + @QmlAnonymous class Person(QObject): name_changed = Signal() - shoe_size_changed = Signal() def __init__(self, parent=None): super().__init__(parent) self._name = '' - self._shoe_size = 0 + self._shoe = ShoeDescription() @Property(str, notify=name_changed, final=True) def name(self): @@ -31,13 +86,9 @@ class Person(QObject): self._name = n self.name_changed.emit() - @Property(int, notify=shoe_size_changed, final=True) - def shoe_size(self): - return self._shoe_size - - @shoe_size.setter - def shoe_size(self, s): - self._shoe_size = s + @Property(ShoeDescription, final=True) + def shoe(self): + return self._shoe @QmlElement diff --git a/examples/qml/tutorials/extending-qml/chapter1-basics/Charts/App.qml b/examples/qml/tutorials/extending-qml/chapter1-basics/Charts/App.qml new file mode 100644 index 00000000..523dc712 --- /dev/null +++ b/examples/qml/tutorials/extending-qml/chapter1-basics/Charts/App.qml @@ -0,0 +1,22 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import Charts +import QtQuick + +Item { + width: 300; height: 200 + + PieChart { + id: aPieChart + anchors.centerIn: parent + width: 100; height: 100 + name: "A simple pie chart" + color: "red" + } + + Text { + anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; bottomMargin: 20 } + text: aPieChart.name + } +} diff --git a/examples/qml/tutorials/extending-qml/chapter1-basics/Charts/qmldir b/examples/qml/tutorials/extending-qml/chapter1-basics/Charts/qmldir new file mode 100644 index 00000000..78602c6b --- /dev/null +++ b/examples/qml/tutorials/extending-qml/chapter1-basics/Charts/qmldir @@ -0,0 +1,4 @@ +module Charts +typeinfo chapter1-basics.qmltypes +depends QtQuick +App 254.0 App.qml diff --git a/examples/qml/tutorials/extending-qml/chapter1-basics/app.qml b/examples/qml/tutorials/extending-qml/chapter1-basics/app.qml deleted file mode 100644 index 6feef563..00000000 --- a/examples/qml/tutorials/extending-qml/chapter1-basics/app.qml +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (C) 2016 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -import Charts -import QtQuick - -Item { - width: 300; height: 200 - - PieChart { - id: aPieChart - anchors.centerIn: parent - width: 100; height: 100 - name: "A simple pie chart" - color: "red" - } - - Text { - anchors { - bottom: parent.bottom; - horizontalCenter: parent.horizontalCenter; - bottomMargin: 20 - } - text: aPieChart.name - } -} diff --git a/examples/qml/tutorials/extending-qml/chapter1-basics/basics.py b/examples/qml/tutorials/extending-qml/chapter1-basics/basics.py index 322bef95..08fa91c1 100644 --- a/examples/qml/tutorials/extending-qml/chapter1-basics/basics.py +++ b/examples/qml/tutorials/extending-qml/chapter1-basics/basics.py @@ -4,11 +4,10 @@ from __future__ import annotations """PySide6 port of the qml/tutorials/extending-qml/chapter1-basics example from Qt v5.x""" -import os from pathlib import Path import sys -from PySide6.QtCore import Property, Signal, QUrl +from PySide6.QtCore import Property, Signal from PySide6.QtGui import QGuiApplication, QPen, QPainter, QColor from PySide6.QtQml import QmlElement from PySide6.QtQuick import QQuickPaintedItem, QQuickView @@ -57,8 +56,8 @@ if __name__ == '__main__': view = QQuickView() view.setResizeMode(QQuickView.ResizeMode.SizeRootObjectToView) - qml_file = os.fspath(Path(__file__).resolve().parent / 'app.qml') - view.setSource(QUrl.fromLocalFile(qml_file)) + view.engine().addImportPath(Path(__file__).parent) + view.loadFromModule("Charts", "App") if view.status() == QQuickView.Status.Error: sys.exit(-1) view.show() diff --git a/examples/qml/tutorials/extending-qml/chapter1-basics/chapter1-basics.pyproject b/examples/qml/tutorials/extending-qml/chapter1-basics/chapter1-basics.pyproject index 869556bb..2207b834 100644 --- a/examples/qml/tutorials/extending-qml/chapter1-basics/chapter1-basics.pyproject +++ b/examples/qml/tutorials/extending-qml/chapter1-basics/chapter1-basics.pyproject @@ -1,3 +1,3 @@ { - "files": ["basics.py", "app.qml"] + "files": ["basics.py", "Charts/App.qml", "Charts/qmldir"] } diff --git a/examples/qml/tutorials/extending-qml/chapter1-basics/doc/chapter1-basics.rst b/examples/qml/tutorials/extending-qml/chapter1-basics/doc/chapter1-basics.rst index d9bc18cc..4963b446 100644 --- a/examples/qml/tutorials/extending-qml/chapter1-basics/doc/chapter1-basics.rst +++ b/examples/qml/tutorials/extending-qml/chapter1-basics/doc/chapter1-basics.rst @@ -72,19 +72,19 @@ drawing operations with the ``QPainter`` API, we can just subclass The ``PieChart`` class defines the two properties, ``name`` and ``color``, with the ``Property`` decorator, and overrides ``QQuickPaintedItem.paint()``. The ``PieChart`` class is registered using the :deco:`~PySide6.QtQml.QmlElement` -decorator, to allow it to be used from QML. If you don't register the class, ``app.qml`` +decorator, to allow it to be used from QML. If you don't register the class, ``App.qml`` won't be able to create a ``PieChart``. QML Usage --------- Now that we have defined the ``PieChart`` type, we will use it from QML. The -``app.qml`` file creates a ``PieChart`` item and displays the pie chart's details +``App.qml`` file creates a ``PieChart`` item and displays the pie chart's details using a standard QML ``Text`` item: -.. literalinclude:: app.qml +.. literalinclude:: Charts/App.qml :lineno-start: 7 - :lines: 7-26 + :lines: 7-22 Notice that although the color is specified as a string in QML, it is automatically converted to a :class:`~PySide6.QtGui.QColor` object for the PieChart @@ -93,7 +93,7 @@ For example, a string like "640x480" can be automatically converted to a ``QSize`` value. We'll also create a main function that uses a :class:`~PySide6.QtQuick.QQuickView` -to run and display ``app.qml``. Here is the application ``basics.py``: +to run and display ``App.qml``. Here is the application ``basics.py``: .. literalinclude:: basics.py :lineno-start: 54 diff --git a/examples/qml/tutorials/extending-qml/chapter2-methods/Charts/App.qml b/examples/qml/tutorials/extending-qml/chapter2-methods/Charts/App.qml new file mode 100644 index 00000000..6190cbc5 --- /dev/null +++ b/examples/qml/tutorials/extending-qml/chapter2-methods/Charts/App.qml @@ -0,0 +1,28 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import Charts +import QtQuick + +Item { + width: 300; height: 200 + + PieChart { + id: aPieChart + anchors.centerIn: parent + width: 100; height: 100 + color: "red" + + onChartCleared: console.log("The chart has been cleared") + } + + MouseArea { + anchors.fill: parent + onClicked: aPieChart.clearChart() + } + + Text { + anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; bottomMargin: 20 } + text: "Click anywhere to clear the chart" + } +} diff --git a/examples/qml/tutorials/extending-qml/chapter2-methods/Charts/qmldir b/examples/qml/tutorials/extending-qml/chapter2-methods/Charts/qmldir new file mode 100644 index 00000000..dad53787 --- /dev/null +++ b/examples/qml/tutorials/extending-qml/chapter2-methods/Charts/qmldir @@ -0,0 +1,4 @@ +module Charts +typeinfo chapter2-methods.qmltypes +depends QtQuick +App 254.0 App.qml diff --git a/examples/qml/tutorials/extending-qml/chapter2-methods/app.qml b/examples/qml/tutorials/extending-qml/chapter2-methods/app.qml deleted file mode 100644 index d9477e25..00000000 --- a/examples/qml/tutorials/extending-qml/chapter2-methods/app.qml +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2016 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -import Charts -import QtQuick - -Item { - width: 300; height: 200 - - PieChart { - id: aPieChart - anchors.centerIn: parent - width: 100; height: 100 - color: "red" - - onChartCleared: console.log("The chart has been cleared") - } - - MouseArea { - anchors.fill: parent - onClicked: aPieChart.clearChart() - } - - Text { - anchors { - bottom: parent.bottom; - horizontalCenter: parent.horizontalCenter; - bottomMargin: 20 - } - text: "Click anywhere to clear the chart" - } -} diff --git a/examples/qml/tutorials/extending-qml/chapter2-methods/chapter2-methods.pyproject b/examples/qml/tutorials/extending-qml/chapter2-methods/chapter2-methods.pyproject index cdf33be7..b0942a27 100644 --- a/examples/qml/tutorials/extending-qml/chapter2-methods/chapter2-methods.pyproject +++ b/examples/qml/tutorials/extending-qml/chapter2-methods/chapter2-methods.pyproject @@ -1,3 +1,3 @@ { - "files": ["methods.py", "app.qml"] + "files": ["methods.py", "Charts/App.qml", "Charts/qmldir"] } diff --git a/examples/qml/tutorials/extending-qml/chapter2-methods/doc/chapter2-methods.rst b/examples/qml/tutorials/extending-qml/chapter2-methods/doc/chapter2-methods.rst index c192fb9b..0d8538b8 100644 --- a/examples/qml/tutorials/extending-qml/chapter2-methods/doc/chapter2-methods.rst +++ b/examples/qml/tutorials/extending-qml/chapter2-methods/doc/chapter2-methods.rst @@ -5,12 +5,12 @@ This is the second of a series of 6 examples forming a tutorial about extending QML with Python. Suppose we want ``PieChart`` to have a ``clearChart()`` method that erases the -chart and then emits a ``chartCleared`` signal. Our ``app.qml`` would be able +chart and then emits a ``chartCleared`` signal. Our ``App.qml`` would be able to call ``clearChart()`` and receive ``chartCleared()`` signals like this: -.. literalinclude:: app.qml +.. literalinclude:: Charts/App.qml :lineno-start: 4 - :lines: 4-32 + :lines: 4-28 To do this, we add a ``clearChart()`` method and a ``chartCleared()`` signal to our C++ class: diff --git a/examples/qml/tutorials/extending-qml/chapter2-methods/methods.py b/examples/qml/tutorials/extending-qml/chapter2-methods/methods.py index 238225fd..02f600d1 100644 --- a/examples/qml/tutorials/extending-qml/chapter2-methods/methods.py +++ b/examples/qml/tutorials/extending-qml/chapter2-methods/methods.py @@ -4,11 +4,10 @@ from __future__ import annotations """PySide6 port of the qml/tutorials/extending-qml/chapter2-methods example from Qt v5.x""" -import os from pathlib import Path import sys -from PySide6.QtCore import Property, Signal, Slot, Qt, QUrl +from PySide6.QtCore import Property, Signal, Slot, Qt from PySide6.QtGui import QGuiApplication, QPen, QPainter, QColor from PySide6.QtQml import QmlElement from PySide6.QtQuick import QQuickPaintedItem, QQuickView @@ -64,8 +63,8 @@ if __name__ == '__main__': view = QQuickView() view.setResizeMode(QQuickView.ResizeMode.SizeRootObjectToView) - qml_file = os.fspath(Path(__file__).resolve().parent / 'app.qml') - view.setSource(QUrl.fromLocalFile(qml_file)) + view.engine().addImportPath(Path(__file__).parent) + view.loadFromModule("Charts", "App") if view.status() == QQuickView.Status.Error: sys.exit(-1) view.show() diff --git a/examples/qml/tutorials/extending-qml/chapter3-bindings/Charts/App.qml b/examples/qml/tutorials/extending-qml/chapter3-bindings/Charts/App.qml new file mode 100644 index 00000000..0e30ba69 --- /dev/null +++ b/examples/qml/tutorials/extending-qml/chapter3-bindings/Charts/App.qml @@ -0,0 +1,36 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import Charts +import QtQuick + +Item { + width: 300; height: 200 + + Row { + anchors.centerIn: parent + spacing: 20 + + PieChart { + id: chartA + width: 100; height: 100 + color: "red" + } + + PieChart { + id: chartB + width: 100; height: 100 + color: chartA.color + } + } + + MouseArea { + anchors.fill: parent + onClicked: { chartA.color = "blue" } + } + + Text { + anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; bottomMargin: 20 } + text: "Click anywhere to change the chart color" + } +} diff --git a/examples/qml/tutorials/extending-qml/chapter3-bindings/Charts/qmldir b/examples/qml/tutorials/extending-qml/chapter3-bindings/Charts/qmldir new file mode 100644 index 00000000..f2d39dd1 --- /dev/null +++ b/examples/qml/tutorials/extending-qml/chapter3-bindings/Charts/qmldir @@ -0,0 +1,4 @@ +module Charts +typeinfo chapter3-bindings.qmltypes +depends QtQuick +App 254.0 App.qml diff --git a/examples/qml/tutorials/extending-qml/chapter3-bindings/app.qml b/examples/qml/tutorials/extending-qml/chapter3-bindings/app.qml deleted file mode 100644 index f1530516..00000000 --- a/examples/qml/tutorials/extending-qml/chapter3-bindings/app.qml +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2016 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -import Charts -import QtQuick - -Item { - width: 300; height: 200 - - Row { - anchors.centerIn: parent - spacing: 20 - - PieChart { - id: chartA - width: 100; height: 100 - color: "red" - } - - PieChart { - id: chartB - width: 100; height: 100 - color: chartA.color - } - } - - MouseArea { - anchors.fill: parent - onClicked: { chartA.color = "blue" } - } - - Text { - anchors { - bottom: parent.bottom; - horizontalCenter: parent.horizontalCenter; - bottomMargin: 20 - } - text: "Click anywhere to change the chart color" - } -} diff --git a/examples/qml/tutorials/extending-qml/chapter3-bindings/bindings.py b/examples/qml/tutorials/extending-qml/chapter3-bindings/bindings.py index ed332cba..a3fa77ed 100644 --- a/examples/qml/tutorials/extending-qml/chapter3-bindings/bindings.py +++ b/examples/qml/tutorials/extending-qml/chapter3-bindings/bindings.py @@ -4,11 +4,10 @@ from __future__ import annotations """PySide6 port of the qml/tutorials/extending-qml/chapter3-bindings example from Qt v5.x""" -import os from pathlib import Path import sys -from PySide6.QtCore import Property, Signal, Slot, QUrl, Qt +from PySide6.QtCore import Property, Signal, Slot, Qt from PySide6.QtGui import QGuiApplication, QPen, QPainter, QColor from PySide6.QtQml import QmlElement from PySide6.QtQuick import QQuickPaintedItem, QQuickView @@ -68,8 +67,8 @@ if __name__ == '__main__': view = QQuickView() view.setResizeMode(QQuickView.ResizeMode.SizeRootObjectToView) - qml_file = os.fspath(Path(__file__).resolve().parent / 'app.qml') - view.setSource(QUrl.fromLocalFile(qml_file)) + view.engine().addImportPath(Path(__file__).parent) + view.loadFromModule("Charts", "App") if view.status() == QQuickView.Status.Error: sys.exit(-1) view.show() diff --git a/examples/qml/tutorials/extending-qml/chapter3-bindings/chapter3-bindings.pyproject b/examples/qml/tutorials/extending-qml/chapter3-bindings/chapter3-bindings.pyproject index 6e21f86f..ebd65a02 100644 --- a/examples/qml/tutorials/extending-qml/chapter3-bindings/chapter3-bindings.pyproject +++ b/examples/qml/tutorials/extending-qml/chapter3-bindings/chapter3-bindings.pyproject @@ -1,3 +1,3 @@ { - "files": ["app.qml", "bindings.py"] + "files": ["bindings.py", "Charts/App.qml", "Charts/qmldir"] } diff --git a/examples/qml/tutorials/extending-qml/chapter3-bindings/doc/chapter3-bindings.rst b/examples/qml/tutorials/extending-qml/chapter3-bindings/doc/chapter3-bindings.rst index 8d29c07a..cba65e2d 100644 --- a/examples/qml/tutorials/extending-qml/chapter3-bindings/doc/chapter3-bindings.rst +++ b/examples/qml/tutorials/extending-qml/chapter3-bindings/doc/chapter3-bindings.rst @@ -11,9 +11,9 @@ other types' values when property values are changed. Let's enable property bindings for the ``color`` property. That means if we have code like this: -.. literalinclude:: app.qml +.. literalinclude:: Charts/App.qml :lineno-start: 7 - :lines: 7-40 + :lines: 7-36 The ``color: chartA.color`` statement binds the ``color`` value of ``chartB`` to the ``color`` of ``chartA.`` Whenever ``chartA`` 's ``color`` value changes, diff --git a/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/Charts/App.qml b/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/Charts/App.qml new file mode 100644 index 00000000..eb0a3cdc --- /dev/null +++ b/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/Charts/App.qml @@ -0,0 +1,22 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import Charts +import QtQuick + +Item { + width: 300; height: 200 + + PieChart { + id: chart + anchors.centerIn: parent + width: 100; height: 100 + + pieSlice: PieSlice { + anchors.fill: parent + color: "red" + } + } + + Component.onCompleted: console.log("The pie is colored " + chart.pieSlice.color) +} diff --git a/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/Charts/qmldir b/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/Charts/qmldir new file mode 100644 index 00000000..7a7a4188 --- /dev/null +++ b/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/Charts/qmldir @@ -0,0 +1,4 @@ +module Charts +typeinfo chapter4-customPropertyTypes.qmltypes +depends QtQuick +App 254.0 App.qml diff --git a/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/app.qml b/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/app.qml deleted file mode 100644 index a5c5ff9f..00000000 --- a/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/app.qml +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (C) 2016 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -import Charts -import QtQuick - -Item { - width: 300; height: 200 - - PieChart { - id: chart - anchors.centerIn: parent - width: 100; height: 100 - - pieSlice: PieSlice { - anchors.fill: parent - color: "red" - } - } - - Component.onCompleted: console.log("The pie is colored " + chart.pieSlice.color) -} diff --git a/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/chapter4-customPropertyTypes.pyproject b/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/chapter4-customPropertyTypes.pyproject index af1cfefb..076f3a82 100644 --- a/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/chapter4-customPropertyTypes.pyproject +++ b/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/chapter4-customPropertyTypes.pyproject @@ -1,3 +1,3 @@ { - "files": ["app.qml", "customPropertyTypes.py"] + "files": ["customPropertyTypes.py", "Charts/App.qml", "Charts/qmldir"] } diff --git a/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/customPropertyTypes.py b/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/customPropertyTypes.py index bf24ec3c..2d03b5cb 100644 --- a/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/customPropertyTypes.py +++ b/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/customPropertyTypes.py @@ -5,11 +5,10 @@ from __future__ import annotations """PySide6 port of the qml/tutorials/extending-qml/chapter4-customPropertyTypes example from Qt v5.x""" -import os from pathlib import Path import sys -from PySide6.QtCore import Property, QUrl +from PySide6.QtCore import Property from PySide6.QtGui import QGuiApplication, QPen, QPainter, QColor from PySide6.QtQml import QmlElement from PySide6.QtQuick import QQuickPaintedItem, QQuickView, QQuickItem @@ -72,8 +71,8 @@ if __name__ == '__main__': view = QQuickView() view.setResizeMode(QQuickView.ResizeMode.SizeRootObjectToView) - qml_file = os.fspath(Path(__file__).resolve().parent / 'app.qml') - view.setSource(QUrl.fromLocalFile(qml_file)) + view.engine().addImportPath(Path(__file__).parent) + view.loadFromModule("Charts", "App") if view.status() == QQuickView.Status.Error: sys.exit(-1) view.show() diff --git a/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/doc/chapter4-customPropertyTypes.rst b/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/doc/chapter4-customPropertyTypes.rst index 394f8261..2a363864 100644 --- a/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/doc/chapter4-customPropertyTypes.rst +++ b/examples/qml/tutorials/extending-qml/chapter4-customPropertyTypes/doc/chapter4-customPropertyTypes.rst @@ -41,7 +41,7 @@ For example, let's replace the use of the ``property`` with a type called ``PieSlice`` that has a ``color`` property. Instead of assigning a color, we assign an ``PieSlice`` value which itself contains a ``color``: -.. literalinclude:: app.qml +.. literalinclude:: Charts/App.qml :lineno-start: 4 :lines: 4-22 diff --git a/examples/qml/tutorials/extending-qml/chapter5-listproperties/Charts/App.qml b/examples/qml/tutorials/extending-qml/chapter5-listproperties/Charts/App.qml new file mode 100644 index 00000000..c0c3e826 --- /dev/null +++ b/examples/qml/tutorials/extending-qml/chapter5-listproperties/Charts/App.qml @@ -0,0 +1,39 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma ComponentBehavior: Bound +import Charts +import QtQuick + +Item { + width: 300; height: 200 + + PieChart { + id: chart + anchors.centerIn: parent + width: 100; height: 100 + + component Slice: PieSlice { + parent: chart + anchors.fill: parent + } + + slices: [ + Slice { + color: "red" + fromAngle: 0 + angleSpan: 110 + }, + Slice { + color: "black" + fromAngle: 110 + angleSpan: 50 + }, + Slice { + color: "blue" + fromAngle: 160 + angleSpan: 100 + } + ] + } +} diff --git a/examples/qml/tutorials/extending-qml/chapter5-listproperties/Charts/qmldir b/examples/qml/tutorials/extending-qml/chapter5-listproperties/Charts/qmldir new file mode 100644 index 00000000..48ec2434 --- /dev/null +++ b/examples/qml/tutorials/extending-qml/chapter5-listproperties/Charts/qmldir @@ -0,0 +1,4 @@ +module Charts +typeinfo chapter5-listproperties.qmltypes +depends QtQuick +App 254.0 App.qml diff --git a/examples/qml/tutorials/extending-qml/chapter5-listproperties/app.qml b/examples/qml/tutorials/extending-qml/chapter5-listproperties/app.qml deleted file mode 100644 index ac99d5a4..00000000 --- a/examples/qml/tutorials/extending-qml/chapter5-listproperties/app.qml +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2016 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -import Charts -import QtQuick - -Item { - width: 300; height: 200 - - PieChart { - anchors.centerIn: parent - width: 100; height: 100 - - slices: [ - PieSlice { - anchors.fill: parent - color: "red" - fromAngle: 0; angleSpan: 110 - }, - PieSlice { - anchors.fill: parent - color: "black" - fromAngle: 110; angleSpan: 50 - }, - PieSlice { - anchors.fill: parent - color: "blue" - fromAngle: 160; angleSpan: 100 - } - ] - } -} diff --git a/examples/qml/tutorials/extending-qml/chapter5-listproperties/chapter5-listproperties.pyproject b/examples/qml/tutorials/extending-qml/chapter5-listproperties/chapter5-listproperties.pyproject index a3f89d57..d726432f 100644 --- a/examples/qml/tutorials/extending-qml/chapter5-listproperties/chapter5-listproperties.pyproject +++ b/examples/qml/tutorials/extending-qml/chapter5-listproperties/chapter5-listproperties.pyproject @@ -1,3 +1,3 @@ { - "files": ["app.qml", "listproperties.py"] + "files": ["listproperties.py", "Charts/App.qml", "Charts/qmldir"] } diff --git a/examples/qml/tutorials/extending-qml/chapter5-listproperties/doc/chapter5-listproperties.rst b/examples/qml/tutorials/extending-qml/chapter5-listproperties/doc/chapter5-listproperties.rst index ece79e9d..980024ee 100644 --- a/examples/qml/tutorials/extending-qml/chapter5-listproperties/doc/chapter5-listproperties.rst +++ b/examples/qml/tutorials/extending-qml/chapter5-listproperties/doc/chapter5-listproperties.rst @@ -8,7 +8,7 @@ Right now, a ``PieChart`` can only have one ``PieSlice.`` Ideally a chart would have multiple slices, with different colors and sizes. To do this, we could have a ``slices`` property that accepts a list of ``PieSlice`` items: -.. literalinclude:: app.qml +.. literalinclude:: Charts/App.qml :lineno-start: 4 :lines: 4-32 diff --git a/examples/qml/tutorials/extending-qml/chapter5-listproperties/listproperties.py b/examples/qml/tutorials/extending-qml/chapter5-listproperties/listproperties.py index 67d7482e..c2ea85ff 100644 --- a/examples/qml/tutorials/extending-qml/chapter5-listproperties/listproperties.py +++ b/examples/qml/tutorials/extending-qml/chapter5-listproperties/listproperties.py @@ -4,11 +4,10 @@ from __future__ import annotations """PySide6 port of the qml/tutorials/extending-qml/chapter5-listproperties example from Qt v5.x""" -import os from pathlib import Path import sys -from PySide6.QtCore import Property, QUrl +from PySide6.QtCore import Property from PySide6.QtGui import QGuiApplication, QPen, QPainter, QColor from PySide6.QtQml import QmlElement, ListProperty from PySide6.QtQuick import QQuickPaintedItem, QQuickView, QQuickItem @@ -86,8 +85,8 @@ if __name__ == '__main__': view = QQuickView() view.setResizeMode(QQuickView.ResizeMode.SizeRootObjectToView) - qml_file = os.fspath(Path(__file__).resolve().parent / 'app.qml') - view.setSource(QUrl.fromLocalFile(qml_file)) + view.engine().addImportPath(Path(__file__).parent) + view.loadFromModule("Charts", "App") if view.status() == QQuickView.Status.Error: sys.exit(-1) view.show() diff --git a/examples/qml/tutorials/extending-qml/chapter6-plugins/App.qml b/examples/qml/tutorials/extending-qml/chapter6-plugins/App.qml new file mode 100644 index 00000000..79a9ce9b --- /dev/null +++ b/examples/qml/tutorials/extending-qml/chapter6-plugins/App.qml @@ -0,0 +1,39 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +pragma ComponentBehavior: Bound +import QtQuick +import Charts + +Item { + width: 300; height: 200 + + PieChart { + id: chart + anchors.centerIn: parent + width: 100; height: 100 + + component Slice: PieSlice { + parent: chart + anchors.fill: parent + } + + slices: [ + Slice { + color: "red" + fromAngle: 0 + angleSpan: 110 + }, + Slice { + color: "black" + fromAngle: 110 + angleSpan: 50 + }, + Slice { + color: "blue" + fromAngle: 160 + angleSpan: 100 + } + ] + } +} + diff --git a/examples/qml/tutorials/extending-qml/chapter6-plugins/Charts/plugins.png b/examples/qml/tutorials/extending-qml/chapter6-plugins/Charts/plugins.png deleted file mode 100644 index 8992e89c..00000000 Binary files a/examples/qml/tutorials/extending-qml/chapter6-plugins/Charts/plugins.png and /dev/null differ diff --git a/examples/qml/tutorials/extending-qml/chapter6-plugins/app.qml b/examples/qml/tutorials/extending-qml/chapter6-plugins/app.qml deleted file mode 100644 index 1a4772e1..00000000 --- a/examples/qml/tutorials/extending-qml/chapter6-plugins/app.qml +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -import QtQuick -import Charts 1.0 - -Item { - width: 300; height: 200 - - PieChart { - anchors.centerIn: parent - width: 100; height: 100 - - slices: [ - PieSlice { - anchors.fill: parent - color: "red" - fromAngle: 0; angleSpan: 110 - }, - PieSlice { - anchors.fill: parent - color: "black" - fromAngle: 110; angleSpan: 50 - }, - PieSlice { - anchors.fill: parent - color: "blue" - fromAngle: 160; angleSpan: 100 - } - ] - } -} diff --git a/examples/qml/tutorials/extending-qml/chapter6-plugins/chapter6-plugins.pyproject b/examples/qml/tutorials/extending-qml/chapter6-plugins/chapter6-plugins.pyproject index cc684401..c4d3f4ce 100644 --- a/examples/qml/tutorials/extending-qml/chapter6-plugins/chapter6-plugins.pyproject +++ b/examples/qml/tutorials/extending-qml/chapter6-plugins/chapter6-plugins.pyproject @@ -1,3 +1,3 @@ { - "files": ["app.qml", "Charts/piechart.py", "Charts/pieslice.py"] + "files": ["App.qml", "Charts/piechart.py", "Charts/pieslice.py"] } diff --git a/examples/qml/tutorials/extending-qml/chapter6-plugins/doc/chapter6-plugins.rst b/examples/qml/tutorials/extending-qml/chapter6-plugins/doc/chapter6-plugins.rst index 2320a8fa..3677a71f 100644 --- a/examples/qml/tutorials/extending-qml/chapter6-plugins/doc/chapter6-plugins.rst +++ b/examples/qml/tutorials/extending-qml/chapter6-plugins/doc/chapter6-plugins.rst @@ -21,4 +21,4 @@ Running the Example .. code-block:: shell - pyside6-qml examples/qml/tutorials/extending-qml/chapter6-plugins/app.qml -I examples/qml/tutorials/extending-qml/chapter6-plugins/Charts + pyside6-qml examples/qml/tutorials/extending-qml/chapter6-plugins/App.qml -I examples/qml/tutorials/extending-qml/chapter6-plugins/Charts diff --git a/examples/quick/customitems/painteditem/TextBalloon/textballoon.py b/examples/quick/customitems/painteditem/TextBalloon/textballoon.py new file mode 100644 index 00000000..7ff2a7d8 --- /dev/null +++ b/examples/quick/customitems/painteditem/TextBalloon/textballoon.py @@ -0,0 +1,56 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtGui import QPainter, QBrush, QColor +from PySide6.QtQml import QmlElement +from PySide6.QtCore import QPointF, Qt, Property, Signal +from PySide6.QtQuick import QQuickPaintedItem + +QML_IMPORT_NAME = "TextBalloon" +QML_IMPORT_MAJOR_VERSION = 1 +QML_IMPORT_MINOR_VERSION = 0 # Optional + + +@QmlElement +class TextBalloon(QQuickPaintedItem): + + rightAlignedChanged = Signal() + + def __init__(self, parent=None): + self._rightAligned = False + super().__init__(parent) + + @Property(bool, notify=rightAlignedChanged) + def rightAligned(self): + return self._rightAligned + + @rightAligned.setter + def rightAligned(self, value): + self._rightAligned = value + self.rightAlignedChanged.emit() + + def paint(self, painter: QPainter): + + brush = QBrush(QColor("#007430")) + + painter.setBrush(brush) + painter.setPen(Qt.PenStyle.NoPen) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + itemSize = self.size() + + painter.drawRoundedRect(0, 0, itemSize.width(), itemSize.height() - 10, 10, 10) + + if self.rightAligned: + points = [ + QPointF(itemSize.width() - 10.0, itemSize.height() - 10.0), + QPointF(itemSize.width() - 20.0, itemSize.height()), + QPointF(itemSize.width() - 30.0, itemSize.height() - 10.0), + ] + else: + points = [ + QPointF(10.0, itemSize.height() - 10.0), + QPointF(20.0, itemSize.height()), + QPointF(30.0, itemSize.height() - 10.0), + ] + painter.drawConvexPolygon(points) diff --git a/examples/quick/customitems/painteditem/doc/painteditem.png b/examples/quick/customitems/painteditem/doc/painteditem.png new file mode 100644 index 00000000..743cd11b Binary files /dev/null and b/examples/quick/customitems/painteditem/doc/painteditem.png differ diff --git a/examples/quick/customitems/painteditem/doc/painteditem.rst b/examples/quick/customitems/painteditem/doc/painteditem.rst new file mode 100644 index 00000000..5c3e8935 --- /dev/null +++ b/examples/quick/customitems/painteditem/doc/painteditem.rst @@ -0,0 +1,13 @@ +Scene Graph Painted Item Example +================================ + +.. tags:: Android + +Shows how to implement QPainter-based custom scenegraph items. + +The Painted Item example shows how to use the QML Scene Graph framework to +implement custom scenegraph items using QPainter. + +.. image:: painteditem.png + :width: 400 + :alt: Painted Item Screenshot diff --git a/examples/quick/customitems/painteditem/main.py b/examples/quick/customitems/painteditem/main.py new file mode 100644 index 00000000..fa846b69 --- /dev/null +++ b/examples/quick/customitems/painteditem/main.py @@ -0,0 +1,39 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from argparse import ArgumentParser, RawTextHelpFormatter +from pathlib import Path +import sys + +from PySide6.QtGui import QGuiApplication +from PySide6.QtCore import QCoreApplication +from PySide6.QtQml import QQmlDebuggingEnabler +from PySide6.QtQuick import QQuickView + +from TextBalloon.textballoon import TextBalloon # noqa: F401 + +if __name__ == "__main__": + argument_parser = ArgumentParser(description="Scene Graph Painted Item Example", + formatter_class=RawTextHelpFormatter) + argument_parser.add_argument("-qmljsdebugger", action="store", + help="Enable QML debugging") + options = argument_parser.parse_args() + if options.qmljsdebugger: + QQmlDebuggingEnabler.enableDebugging(True) + + app = QGuiApplication(sys.argv) + QCoreApplication.setOrganizationName("QtProject") + QCoreApplication.setOrganizationDomain("qt-project.org") + + view = QQuickView() + view.setResizeMode(QQuickView.ResizeMode.SizeRootObjectToView) + view.engine().addImportPath(Path(__file__).parent) + view.loadFromModule("painteditemexample", "Main") + + if view.status() == QQuickView.Status.Error: + sys.exit(-1) + view.show() + + exit_code = QCoreApplication.exec() + del view + sys.exit(exit_code) diff --git a/examples/quick/customitems/painteditem/painteditem.pyproject b/examples/quick/customitems/painteditem/painteditem.pyproject new file mode 100644 index 00000000..ffe340ea --- /dev/null +++ b/examples/quick/customitems/painteditem/painteditem.pyproject @@ -0,0 +1,4 @@ +{ + "files": ["main.py", "painteditemexample/Main.qml", "painteditemexample/qmldir", + "TextBalloon/textballoon.py"] +} diff --git a/examples/quick/customitems/painteditem/painteditemexample/Main.qml b/examples/quick/customitems/painteditem/painteditemexample/Main.qml new file mode 100644 index 00000000..69be4a01 --- /dev/null +++ b/examples/quick/customitems/painteditem/painteditemexample/Main.qml @@ -0,0 +1,72 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import TextBalloon + +Item { + height: 480 + width: 320 + + //! [0] + ListModel { + id: balloonModel + ListElement { + balloonWidth: 200 + } + ListElement { + balloonWidth: 120 + } + } + + ListView { + id: balloonView + anchors.bottom: controls.top + anchors.bottomMargin: 2 + anchors.top: parent.top + delegate: TextBalloon { + anchors.right: index % 2 !== 0 ? parent?.right : undefined + height: 60 + rightAligned: index % 2 !== 0 + width: balloonWidth + } + model: balloonModel + spacing: 5 + width: parent.width + } + //! [0] + + //! [1] + Rectangle { + id: controls + + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.margins: 1 + anchors.right: parent.right + border.width: 2 + color: "white" + height: parent.height * 0.15 + + Text { + anchors.centerIn: parent + text: qsTr("Add another balloon") + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onClicked: { + balloonModel.append({"balloonWidth": Math.floor(Math.random() * 200 + 100)}) + balloonView.positionViewAtIndex(balloonView.count -1, ListView.End) + } + onEntered: { + parent.color = "#8ac953" + } + onExited: { + parent.color = "white" + } + } + } + //! [1] +} diff --git a/examples/quick/customitems/painteditem/painteditemexample/qmldir b/examples/quick/customitems/painteditem/painteditemexample/qmldir new file mode 100644 index 00000000..b33630ba --- /dev/null +++ b/examples/quick/customitems/painteditem/painteditemexample/qmldir @@ -0,0 +1,2 @@ +module painteditemexample +Main 1.0 Main.qml diff --git a/examples/quick/models/objectlistmodel/view.qml b/examples/quick/models/objectlistmodel/view.qml index b7cf68a9..3cb072dd 100644 --- a/examples/quick/models/objectlistmodel/view.qml +++ b/examples/quick/models/objectlistmodel/view.qml @@ -2,14 +2,20 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import QtQuick +import QtQuick.Controls ListView { - width: 100; height: 100 + id: listview + width: 200; height: 320 + required model + ScrollBar.vertical: ScrollBar { } delegate: Rectangle { - color: model.modelData.color - height: 25 - width: 100 - Text { text: model.modelData.name } + width: listview.width; height: 25 + + required color + required property string name + + Text { text: parent.name } } } diff --git a/examples/quick/painteditem/doc/painteditem.png b/examples/quick/painteditem/doc/painteditem.png deleted file mode 100644 index 743cd11b..00000000 Binary files a/examples/quick/painteditem/doc/painteditem.png and /dev/null differ diff --git a/examples/quick/painteditem/doc/painteditem.rst b/examples/quick/painteditem/doc/painteditem.rst deleted file mode 100644 index 5c3e8935..00000000 --- a/examples/quick/painteditem/doc/painteditem.rst +++ /dev/null @@ -1,13 +0,0 @@ -Scene Graph Painted Item Example -================================ - -.. tags:: Android - -Shows how to implement QPainter-based custom scenegraph items. - -The Painted Item example shows how to use the QML Scene Graph framework to -implement custom scenegraph items using QPainter. - -.. image:: painteditem.png - :width: 400 - :alt: Painted Item Screenshot diff --git a/examples/quick/painteditem/main.qml b/examples/quick/painteditem/main.qml deleted file mode 100644 index 44be89dc..00000000 --- a/examples/quick/painteditem/main.qml +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (C) 2020 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -import QtQuick -import TextBalloonPlugin - -Item { - height: 480 - width: 320 - - //! [0] - ListModel { - id: balloonModel - ListElement { - balloonWidth: 200 - } - ListElement { - balloonWidth: 120 - } - } - - ListView { - anchors.bottom: controls.top - anchors.bottomMargin: 2 - anchors.top: parent.top - id: balloonView - delegate: TextBalloon { - anchors.right: index % 2 == 0 ? undefined : balloonView.contentItem.right - height: 60 - rightAligned: index % 2 == 0 ? false : true - width: balloonWidth - } - model: balloonModel - spacing: 5 - width: parent.width - } - //! [0] - - //! [1] - Rectangle { - id: controls - - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.margins: 1 - anchors.right: parent.right - border.width: 2 - color: "white" - height: parent.height * 0.15 - - Text { - anchors.centerIn: parent - text: "Add another balloon" - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onClicked: { - balloonModel.append({ - "balloonWidth": Math.floor( - Math.random( - ) * 200 + 100) - }) - balloonView.positionViewAtIndex(balloonView.count - 1, - ListView.End) - } - onEntered: { - parent.color = "#8ac953" - } - onExited: { - parent.color = "white" - } - } - } - //! [1] -} diff --git a/examples/quick/painteditem/painteditem.py b/examples/quick/painteditem/painteditem.py deleted file mode 100644 index 95390b6e..00000000 --- a/examples/quick/painteditem/painteditem.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright (C) 2022 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from __future__ import annotations - -from argparse import ArgumentParser, RawTextHelpFormatter -from pathlib import Path -import sys - -from PySide6.QtGui import QPainter, QBrush, QColor -from PySide6.QtWidgets import QApplication -from PySide6.QtQml import QmlElement, QQmlDebuggingEnabler -from PySide6.QtCore import QUrl, Property, Signal, Qt, QPointF -from PySide6.QtQuick import QQuickPaintedItem, QQuickView - -QML_IMPORT_NAME = "TextBalloonPlugin" -QML_IMPORT_MAJOR_VERSION = 1 -QML_IMPORT_MINOR_VERSION = 0 # Optional - - -@QmlElement -class TextBalloon(QQuickPaintedItem): - - rightAlignedChanged = Signal() - - def __init__(self, parent=None): - self._rightAligned = False - super().__init__(parent) - - @Property(bool, notify=rightAlignedChanged) - def rightAligned(self): - return self._rightAligned - - @rightAligned.setter - def rightAligned(self, value): - self._rightAligned = value - self.rightAlignedChanged.emit() - - def paint(self, painter: QPainter): - - brush = QBrush(QColor("#007430")) - - painter.setBrush(brush) - painter.setPen(Qt.PenStyle.NoPen) - painter.setRenderHint(QPainter.RenderHint.Antialiasing) - - itemSize = self.size() - - painter.drawRoundedRect(0, 0, itemSize.width(), itemSize.height() - 10, 10, 10) - - if self.rightAligned: - points = [ - QPointF(itemSize.width() - 10.0, itemSize.height() - 10.0), - QPointF(itemSize.width() - 20.0, itemSize.height()), - QPointF(itemSize.width() - 30.0, itemSize.height() - 10.0), - ] - else: - points = [ - QPointF(10.0, itemSize.height() - 10.0), - QPointF(20.0, itemSize.height()), - QPointF(30.0, itemSize.height() - 10.0), - ] - painter.drawConvexPolygon(points) - - -if __name__ == "__main__": - - argument_parser = ArgumentParser(description="Scene Graph Painted Item Example", - formatter_class=RawTextHelpFormatter) - argument_parser.add_argument("-qmljsdebugger", action="store", - help="Enable QML debugging") - options = argument_parser.parse_args() - if options.qmljsdebugger: - QQmlDebuggingEnabler.enableDebugging(True) - app = QApplication(sys.argv) - view = QQuickView() - view.setResizeMode(QQuickView.ResizeMode.SizeRootObjectToView) - qml_file = Path(__file__).parent / "main.qml" - view.setSource(QUrl.fromLocalFile(qml_file)) - - if view.status() == QQuickView.Status.Error: - sys.exit(-1) - view.show() - - sys.exit(app.exec()) diff --git a/examples/quick/painteditem/painteditem.pyproject b/examples/quick/painteditem/painteditem.pyproject deleted file mode 100644 index 0597c2a9..00000000 --- a/examples/quick/painteditem/painteditem.pyproject +++ /dev/null @@ -1,3 +0,0 @@ -{ - "files": ["main.qml", "painteditem.py"] -} diff --git a/examples/quick/rendercontrol/rendercontrol_opengl/demo.qml b/examples/quick/rendercontrol/rendercontrol_opengl/demo.qml index 00f6a81e..ca6ba7b2 100644 --- a/examples/quick/rendercontrol/rendercontrol_opengl/demo.qml +++ b/examples/quick/rendercontrol/rendercontrol_opengl/demo.qml @@ -2,13 +2,14 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import QtQuick -import QtQuick.Particles 2.0 +import QtQuick.Particles Rectangle { id: root + property bool keyDown: false gradient: Gradient { - GradientStop { position: 0; color: mouse.pressed ? "lightsteelblue" : "steelblue" } + GradientStop { position: 0; color: mouse.pressed ? "lightsteelblue" : (root.keyDown ? "blue" : "steelblue") } GradientStop { position: 1; color: "black" } } @@ -158,4 +159,7 @@ Rectangle { id: mouse anchors.fill: parent } + + Keys.onPressed: keyDown = true + Keys.onReleased: keyDown = false } diff --git a/examples/quick/scenegraph/openglunderqml/main.qml b/examples/quick/scenegraph/openglunderqml/main.qml index 73bfa326..5d4e51f7 100644 --- a/examples/quick/scenegraph/openglunderqml/main.qml +++ b/examples/quick/scenegraph/openglunderqml/main.qml @@ -30,7 +30,7 @@ Item { id: label color: "black" wrapMode: Text.WordWrap - text: "The background here is a squircle rendered with raw OpenGL using the 'beforeRender()' signal in QQuickWindow. This text label and its border is rendered using QML" + text: qsTr("The background here is a squircle rendered with raw OpenGL using the 'beforeRender()' signal in QQuickWindow. This text label and its border is rendered using QML") anchors.right: parent.right anchors.left: parent.left anchors.bottom: parent.bottom diff --git a/examples/quick/scenegraph/scenegraph_customgeometry/main.qml b/examples/quick/scenegraph/scenegraph_customgeometry/main.qml index 88431a17..04430242 100644 --- a/examples/quick/scenegraph/scenegraph_customgeometry/main.qml +++ b/examples/quick/scenegraph/scenegraph_customgeometry/main.qml @@ -22,13 +22,13 @@ Item { p2: Qt.point(t, 1 - t) p3: Qt.point(1 - t, t) } - Text { anchors.bottom: line.bottom + x: 20 width: parent.width - 40 wrapMode: Text.WordWrap - text: "This curve is a custom scene graph item, implemented using GL_LINE_STRIP" + text: qsTr("This curve is a custom scene graph item, implemented using line strips") } } diff --git a/examples/quick/window/Splash.qml b/examples/quick/window/Splash.qml index 0a7da219..69894255 100644 --- a/examples/quick/window/Splash.qml +++ b/examples/quick/window/Splash.qml @@ -24,19 +24,20 @@ Window { Image { id: splashImage source: Images.qtLogo - MouseArea { - anchors.fill: parent - onClicked: Qt.quit() + TapHandler { + onTapped: splash.exit() } } + + function exit() { + splash.visible = false + splash.timeout() + } + //! [timer] Timer { - interval: splash.timeoutInterval; running: true; repeat: false - onTriggered: { - splash.visible = false - splash.timeout() - } + interval: splash.timeoutInterval; running: splash.visible; repeat: false + onTriggered: splash.exit() } //! [timer] - Component.onCompleted: visible = true } diff --git a/examples/quick/window/rc_window.py b/examples/quick/window/rc_window.py index 30b1fbf0..13dd3432 100644 --- a/examples/quick/window/rc_window.py +++ b/examples/quick/window/rc_window.py @@ -1,290 +1,290 @@ # Resource object code (Python 3) # Created by: object code -# Created by: The Resource Compiler for Qt version 6.5.0 +# Created by: The Resource Compiler for Qt version 6.10.0 # WARNING! All changes made in this file will be lost! from PySide6 import QtCore qt_resource_data = b"\ -\x00\x00\x05\x12\ +\x00\x00\x04\xe8\ /\ / Copyright (C) \ 2021 The Qt Comp\ -any Ltd.\x0d\x0a// SPD\ -X-License-Identi\ -fier: LicenseRef\ --Qt-Commercial O\ -R BSD-3-Clause\x0d\x0a\ -\x0d\x0aimport QtQuick\ -\x0d\x0aimport QtQuick\ -.Controls\x0d\x0a\x0d\x0aCol\ -umn {\x0d\x0a id: r\ -oot\x0d\x0a spacing\ -: 8\x0d\x0a\x0d\x0a Label\ - {\x0d\x0a text\ -: \x22Total number \ -of screens: \x22 + \ -screenInfo.count\ -\x0d\x0a font.b\ -old: true\x0d\x0a }\ -\x0d\x0a\x0d\x0a Flow {\x0d\x0a\ +any Ltd.\x0a// SPDX\ +-License-Identif\ +ier: LicenseRef-\ +Qt-Commercial OR\ + BSD-3-Clause\x0a\x0ai\ +mport QtQuick\x0aim\ +port QtQuick.Con\ +trols\x0a\x0aColumn {\x0a\ + id: root\x0a \ + spacing: 8\x0a\x0a \ + Label {\x0a \ + text: \x22Total nu\ +mber of screens:\ + \x22 + screenInfo.\ +count\x0a fo\ +nt.bold: true\x0a \ + }\x0a\x0a Flow {\x0a\ spacing:\ - 12\x0d\x0a wid\ -th: parent.width\ -\x0d\x0a\x0d\x0a Repe\ -ater {\x0d\x0a \ - id: screenIn\ -fo\x0d\x0a \ -model: (Qt.appli\ + 12\x0a widt\ +h: parent.width\x0a\ +\x0a Repeate\ +r {\x0a \ +id: screenInfo\x0a \ + model\ +: (Qt.applicatio\ +n as Application\ +).screens\x0a \ + Label {\x0a \ + re\ +quired property \ +string name\x0a \ + requ\ +ired property in\ +t virtualX\x0a \ + requi\ +red property int\ + virtualY\x0a \ + requir\ +ed property var \ +modelData // avo\ +id shadowing Lab\ +el.width and hei\ +ght\x0a\x0a \ + lineHeight:\ + 1.5\x0a \ + text: name \ ++ \x22\x5cn\x22 + virtual\ +X + \x22, \x22 + virtu\ +alY + \x22 \x22 + mode\ +lData.width + \x22x\ +\x22 + modelData.he\ +ight\x0a \ + }\x0a }\x0a \ + }\x0a\x0a Componen\ +t.onCompleted: {\ +\x0a var scr\ +eens = (Qt.appli\ cation as Applic\ -ation).screens\x0d\x0a\ - Labe\ -l {\x0d\x0a \ - required pr\ -operty string na\ -me\x0d\x0a \ - required pro\ -perty int virtua\ -lX\x0d\x0a \ - required pro\ -perty int virtua\ -lY\x0d\x0a \ - required pro\ -perty var modelD\ -ata // avoid sha\ -dowing Label.wid\ -th and height\x0d\x0a\x0d\ -\x0a \ - lineHeight: 1.5\ -\x0d\x0a \ - text: name + \x22\ -\x5cn\x22 + virtualX +\ - \x22, \x22 + virtualY\ - + \x22 \x22 + modelDa\ -ta.width + \x22x\x22 +\ - modelData.heigh\ -t\x0d\x0a }\ -\x0d\x0a }\x0d\x0a \ - }\x0d\x0a\x0d\x0a Compon\ -ent.onCompleted:\ - {\x0d\x0a var \ -screens = (Qt.ap\ -plication as App\ -lication).screen\ -s;\x0d\x0a for \ -(var i = 0; i < \ -screens.length; \ -++i)\x0d\x0a \ - console.log(\x22s\ -creen \x22 + screen\ -s[i].name + \x22 ha\ -s geometry \x22 +\x0d\x0a\ - \ - screens[\ -i].virtualX + \x22,\ +ation).screens;\x0a\ + for (var\ + i = 0; i < scre\ +ens.length; ++i)\ +\x0a con\ +sole.log(\x22screen\ \x22 + screens[i].\ -virtualY + \x22 \x22 +\ -\x0d\x0a \ - screen\ -s[i].width + \x22x\x22\ - + screens[i].he\ -ight)\x0d\x0a }\x0d\x0a}\x0d\ -\x0a\ -\x00\x00\x04\x8a\ -\x00\ -\x00\x16\xa7x\xda\xcdXYo\xdbF\x10~7\xe0\xff\ -0a_\xec\x06\xba\x93\x17\x15Fa+H\x1d\xc0A\ -b\xcb\xa8\x03\x14}\xa0\xc9\x91\xb8\xf5\x8a+\xec.-\ -9\xae\xff{\x87\xc7\x92\x94\xb8$\xe5ZnJ\x08\x10\ -\xb9;;\xc77\x07g\xd8\xeb\xc1D,\x1f$\x9b\x07\ -\x1a\x8e&\xc70\xec\x0f\x07p\x1d \x5cj\xdaY,\ -\xdd\xf0\x01.\xb4\xdf=<\xe8\xf5`\xfa\xf5\xc3\xb7\xce\ -\x05\xf30T\xd8\xf9\xe4c\xa8\xd9\x8c\xa1\x1cC\xb6v\ -\x85\xb3\xce\xa5\xee\xd0\xb9\x05J\x8f\xb9\x1c\xbe\x5c\xc1\xd9\ -\xf4Cg\xd4\x99p7Rxxpx\xc0\x16K!\ -5\xf1\xbf\x8c\x98w\xb7\xfd\xdc\x9d\x88PK\xc1UL\ -z\xa9\xbf\xdc\xfe\x85\x9e\x86\xc7\xc3\x03\xa0\x8b\xf9c\x90\ -B\xe8\xf4i)\xc5\x12\xa5~\x00\x89$\xc9\xc7\x99\x1b\ -q=]\xba\x1e\x0b\xe7c\x18\xf4\xb7\xa8\xa6\x0fJ\xe3\ -\xe2\xab\xcbQk\x84e\xfa?\xdeZ~\x84\xa7X\xf0\ -\xc6\xc1{W\x82\x97ju\xc3B_\xac\xc6\x90\xfe\x1b\ -\xb5\xe2k\xc5|\x1d\x8c\x89\x8ew\xc9 \xce<\xa6o\ -\xe2%x\x9bh\xdc\xddT\x0f~\x86aq6\xc0\x18\ -\xfe\xcd\xc3\xe7\xc9\xda.\xa7\xe9\x94\x90).\xdd\xcc\xaa\ -\xee*\xd1\xaf\xa0\xd1Ls\xb2\xd5\xc9\xc0\xcd\xf4w\x0a\ -\x82\x89\xe0\xd1\x22,\x1bd\xf0&\xf6\x9b\x8bn\xe8\x05\ -B\xaa\xee\x8cq>&\x1c%\x85\x81\x9db\xe1\xca9\ -\x0b\xd5\xd8f\xc3\xe6\x09e\xbc\xd6J\xb9\xe9t\x0f9\ -\xbf)\x90O\x9c\x00=\x18A\xc7\xb0\xdc<}\xe1\xde\ -\x22''k\x5c\xeb\x12\x1c\x9a\xe2]!\xf9\xd8\x87\x14\ -\xb9\xb1\x13\xc7A\xf9\xe4o\x92\xf9\xdb\xf0\x18\x88\xe6\xb4\ -W\xdd\xf1\x12H\xc9\xfaQuow{K\xb1\x95B\ -\x9d\x1aY%:\x8b\xb4\x16\xa1ME\xa3\xa6\x0a\xc4*\ -\xa5\xb2\xd3\x94B8\xc7\xd5N\x99\xc2\x97\xe8\xaeQ\xe9\ -4\x9a\xba\xf7L\xb1[\x8e\xf0+8\xe7\xccG\x07\x08\ -\xe1i\xb0\x11f\xe5K\x84\x13\x8a\xf4;\xf4\xeb9\x9d\ -\xc0\x9b\x9a\xad*\xcb\xa7\xeaR\xaf\xf7\x06\xfeH\x1d\x8a\ -\xfe$@\xef\xeeV\xac\xff\xac\xd2%[gb]\x07\ -^\x16-7\x19\xa7\x1a{L\x16\x170w\xd3\xa5\x97\ -\xa2}Fr\xe3\xac'\xefz\xb1\xa6H\x81HE\x89\ -GX\x83\x1c\xe3\x8c\x12\xe4\xe4\xe4$\xcb\xf3\xae\xd1\xdc\ -\x06\xd2.\xae\xc8\x18n\xb3\xfbO\x9d\xf0j\xf0f\xde\ -\xfd\x18q\x0eSO\x22\x86\xcek\xf9!\x96\x91\x8a\xd8\ -\x93'\x0a\x86;\xf9\xa2\xbdF\xb8\x91\x16\xfb\xac\x11\xce\ -)1\x5c\xb8\x9ay\xce^\x0c\xce\xd9\xfd\x9eo\xeed\ -\xf9\xae\xd1U\xd8\xdf\x18]\x99q\x9fY\xc8\x16\xec{\ -mIxy\xc4\xe4\x12\xf6\x1409\xbf\x1f\x89\x9a\xbb\ -~m\xd4\x8c\x84}\xa1f\xf8\xb5\xa2\xb6\xf58\x8bB\ -O32\xa4\xe0z-\xa6Z\x92yG\xf7\xc76X\ -\xd5\x8ai/\x80\x9a]\xcfU\xb8]\x85\xc7v\x13%\ -\xeaH\x86\xe0\x98\xd2\xeb\xfc\xd2\xcc.\x8f\x8c\x16~\x8b\ -<\xe6\xdb\x18\x1a\xd0\xda\x18\xe6\xe1\xd0\xc2\xb0(v-\ -\x1cgD\xa8\xd2J\xde\xc2\xd2RNZx\xbby=\ -kaM-\x90\xdf\xaai\x90\x10\xd9XY\x02\xd7\x1c\ -\x8a\xc2\xbbP\xac*\xa7\x9e\xac\xad\xae\xbdc-\xc21\ -!\xab\x12e\xa9\xba\xd1\x14\x03S\xe0\xd0Dr\xd4\xd0\ -\xf8e\xb7I\xef\xc7B\xf3t\x0co\xed8\x001t\ -I@\xe0\xaa\x92N\x89\x94\xac\xdb\xb5$N}\xc6\x1e\ -7\x22rE\x93\xa4\x1b\xceI\xd1Gk\xb7^\x99\xa2\ -b\x10\xfee7n*\xe3\xa0Q\xa3I$c&Y\ -K\xf0\xf8\xffW\xf8\xd4d\xa0\x22m-|\xcb\xf4\xd9\ -\xad}\x9e.\xdc\xd70L\x8f\x86\xfd\xea\x90<|\xd7\ -\xaf\xcc\xbe\xceO\xc3\xc1\xfbw\xfd\xbeS\x9dx\xafI\ -\x90\x91@\xa55H\x8f$\x11\x96\xdc\x15'f\xdc\x9d\ -\xd3\xb4v\xa9\xb3\xca\x0a\x7f\x17\xf7\x1f\xf3\x8a\x92\xbe\xe6\ -\xceYy\xe6\xad\xf5\xd3k\x8c\xca\xb5ymXy$\ -\x04\xe5\xa7\xd0.\xb0\x94\xdc\xd34\xb9+\xdf\x02,~\ -\xff,\x22\x85\xa74r7\x09\xae\xb7\xb2\xe5\x95\x9b\xba\ -\xe4\x84\xdc\x88}o4\xc0f]\xea\x9bX\xa3\x88L\ -#%\x0bLioH\x0c\xb1\x16\xcb\x9c\x94\xee\xeb\x09\ -wwQ\xcb\x88\xdc4\x16P\x09\xc55\xd3Pz\x85\ -\xc5\xa5t.\xca+\xb5)>x\xdfo\x04\xbe\xae\xf1\ -\x9f\xc1\xd1s5=\xae+\xe8\xb0\xbf>>\xbe\x90+\ -|\x99\xa8g\xccH\xcf\x8e4\x8e\xb3\x22\xd0\xe2\x87\x1f\ -\x14g\xce\xb7\xfa\xa0\x18\xf5\x9f\x97\x8c\x5c(<\xaa\x7f\ -\x936\x96u\xb5\xe4\xae\x0aLa\x9f&Oe\xfcD\ -x\xcd\x16(\x22\x93\x1a\x1b\x9fUK\x9f}\xb4\x8c0\ -\x97C\xbf\x7f\x00\xe8k\x05\xe6\ -\x00\x00\x04$\ +name + \x22 has geo\ +metry \x22 +\x0a \ + \ + screens[i].vir\ +tualX + \x22, \x22 + s\ +creens[i].virtua\ +lY + \x22 \x22 +\x0a \ + \ + screens[i].wi\ +dth + \x22x\x22 + scre\ +ens[i].height)\x0a \ + }\x0a}\x0a\ +\x00\x00\x05\x01\ +(\ +\xb5/\xfd`\xff\x16\xbd'\x00\x06p\x912`\x8d:\ +0\x0c\xc3\xc0\x161\x1c\x00\x98G\xc1\xb5\xb6\x93$\xda\ +IN\x11E,I\x22\xd5#\xdck\xe3\x5c\x9d/Y\ +\xc2BlU\xb2\xcdF\x1e\x12Xb\x833\xad\x1c\x83\ +\x00~\x00\x85\x00\x15\xc9\xe1\xec\x8b\xb7\xd4\x8d&Lf\ ++\xd1\xec\xe70\xcf\xcb\xcc\x85\x18\xaetL\xb7\xf4\x94\ +1\xa7\xd8\xbb\xd3\xad`\xef\xeeN \xfb\x8f7@\x13\ +\xdf\x99\xa6*\x9d\xfet\x8b\xe2x\x9du\xbe\xb3O\x1d\ +\xbbz\x9a\xb8fNZ\x96\x8d\xa5\xc1\xc3\xb0\x15\x13+\ +]?\xca\x97\xe5;\x96\x10WlE\xb9C\xfd\x93!\ +\x84\x85\xeb\xe4\xd7\x11\x16\x8c\x06\x0a\xcfD\xc1\x8f\xd8\xd2\ +t\xd8\xe5\xd6~g9~6\xa3\xc1h\xb6bQ\xf6\ +\xa3\xa8i\x09sk/\x97\x1c\x8f\xa3\x059c\x01.\ +\x17\x11\x80M\x80\xcc\xf1ZZ\xd3\xa3\xa7m\xb5\x8da\ +s\xda*,KP\xd7\x1eQ_\xf9I\x1e\xab\xffg\ +\x0c\x9b'\x82M\x1fD\xb6\x91\x16N\x7f\x5c\x97\xe3\xbf\ +\x07\xff\x82\xacd\x90\x8b\xa8k\xd9\xcd^\xf6BJ~\ +\xb5\xaa\xb3E\xd4]?\x99J\xd9!\xaaR\x8fo\xba\ +ye\xd2\xad\xd6\xb6b\xf9\xfd$\x8a\xffY:l+\ +\xea\x1c\x8e\x9a\x15Xj`/\xbe_K\xecjJ\xfa\ +\xdb*>l\xd2\x06_\xf9\xd30\x98\x02\x83\x8c&\xa0\ +\x15h0\x9a\x8d\x08\xc8\xd6\x05\x10\x9aY\x19\x96\xa2\xe9\ +\xfeqE\xa5\xba\xb8p\xbam\x03[\x99M\xd9\x81m\ +[{I>9f\x16!l\x8e\xab\xa1\x0b\xa1\x91\xda\ +N\x9a=\xd3\x8a\xab{\x1b\xdf7\xa9VF\x14\xd1l\ +\xfa\x1e\xff\xec\xf4\xfe7\xb3\x90\x87\x83\xfc\xd1W\xcf\x96\ +\xb0h\xaf\xdf]\xfe\xd9B\xf3\xcf\x5c\x9b\xcc\xf4yV\ +\xca\xcca\x13\x12\x12\xb6\xf4-\x9b3\xe9F\x1c\xdfN\ +\x87\xb3\x19\xa8\x0b\x0fg\xd0\xf0pP\x93H\xe6|\xb5\ +'V'\xb1Z;\xfeV(\x5c]\xbe\x91\x92\x10\xcb\ +\xad\xa8\x7fe\xda(\xf4S;VA\xba\x11\xe5\x9f\x10\ +P:\x07\xce\x85Z\x99Rp\xcd\x9ce\xe9k\xc9I\ +\x86\x90)\x8f\xdbg_<\x0fF<\xd9\x08\xbaXJ\xe8\x8c\xba\ -0\xec\x0f\x07p\xbbD\xb8\x91j$N\x08\xdb\xc0X\ -\x86n\xbb\xe5y0\xbd\xbe\xb8\xeb\x8di\x80,\xc5\xde\ -e\x88L\xd29E\xe1C\xe9\x9b\xe0\xbcw#{*\ -.F\x11P\x12\xc1\xd5\x04>M/zoz\xa3\x88\ -d)\xb6[\xed\x16\x8d\x13.\xa4\xca\x7f\x93\xd1\xe0a\ -\xd7vG\x9cI\xc1\xa3TC/%\xc6\xf0\xbb\xdd\x02\ -\xf5\xd0\xd0\x07\xc1\xb9,\xacG\x1a\xca\xa5\x0fg\xfd~\ -a/Q\xcf\xc0\x87D\xf0\x04\x85\xdc|\x114tU\ -\xe6\x88\x06T~\xcd\x07\xe1\x15\x0c\xde\xe9\xac\x1a?\xcf\ -X )g\xc0\x05U\xd3 \xfa\xfd\x96O\xa5\xa0l\ -\xd1\xe1\xdd\x8aT?\xe9#\x95\xc1\x12v\xbc\x01I\xf5\ -\x1a\xb9\xd7\x82\xc6Dl\xae\xea4~\x0d\xd2\x8f@\x99\ -\x09\x06NR\x00\x9dsK\x0e5\x7fA\xa8l\x90\xa4\ -D\xda\xb2\x8c\x09\x0b\xd3\x80$x\x8cr\x8d\ +T\xf0k\xf8\x0bj\xef?\xd0r\xb57\x1d\x07(\xe7\ +\xee{h\x13\x8b\xc1\xb6\x90\x92\xf9\x8a\xe5\x95)\xd9\x22\ +2\xc6\xc9e\xe7i\xb0R\x1f\x18W\x95lPV#\ +}Mgl\xb3\xb8\xa7\xd8EF\x22\xaa\xe40P8\ +\xf1\xaf\x0f\x82\x0dt\xe6\xa4\x01G@\x07\xa5;^\x0c\ +v\x14\xe4\x90k*\x5c\xfe\x9b\x14o\xd6@9\xd6\xa2\ +\x7f\xb0\x85\x1f\xe7yV2\xe4+\xf4\xcc1S<\x98\ +\x01\x9e - + window.qml Splash.qml CurrentScreen.qml diff --git a/examples/quick3d/customgeometry/CustomGeometryExample/Main.qml b/examples/quick3d/customgeometry/CustomGeometryExample/Main.qml new file mode 100644 index 00000000..ced493e1 --- /dev/null +++ b/examples/quick3d/customgeometry/CustomGeometryExample/Main.qml @@ -0,0 +1,398 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick3D +import QtQuick3D.Helpers + +import CustomGeometryExample + +ApplicationWindow { + id: window + width: 1280 + height: 720 + visible: true + title: "Custom Geometry Example" + + property bool isLandscape: width > height + + View3D { + id: v3d + anchors.left: window.isLandscape ? controlsPane.right : parent.left + anchors.top: window.isLandscape ? parent.top : controlsPane.bottom + anchors.right: parent.right + anchors.bottom: parent.bottom + + camera: camera + + environment: SceneEnvironment { + id: env + backgroundMode: SceneEnvironment.Color + clearColor: "#002b36" + } + + Node { + id: originNode + PerspectiveCamera { + id: cameraNode + z: 600 + } + } + + DirectionalLight { + id: directionalLight + color: Qt.rgba(0.4, 0.2, 0.6, 1.0) + ambientColor: Qt.rgba(0.1, 0.1, 0.1, 1.0) + } + + PointLight { + id: pointLight + position: Qt.vector3d(0, 0, 100) + color: Qt.rgba(0.1, 1.0, 0.1, 1.0) + ambientColor: Qt.rgba(0.2, 0.2, 0.2, 1.0) + } + + Model { + id: gridModel + visible: false + scale: Qt.vector3d(100, 100, 100) + geometry: GridGeometry { + id: grid + horizontalLines: 20 + verticalLines: 20 + } + materials: [ + PrincipledMaterial { + lineWidth: sliderLineWidth.value + } + ] + } + + //! [model triangle] + Model { + id: triangleModel + visible: false + scale: Qt.vector3d(100, 100, 100) + geometry: ExampleTriangleGeometry { + normals: cbNorm.checked + normalXY: sliderNorm.value + uv: cbUV.checked + uvAdjust: sliderUV.value + } + materials: [ + PrincipledMaterial { + Texture { + id: baseColorMap + source: "qt_logo_rect.png" + } + cullMode: PrincipledMaterial.NoCulling + baseColorMap: cbTexture.checked ? baseColorMap : null + specularAmount: 0.5 + } + ] + } + //! [model triangle] + + Model { + id: pointModel + visible: false + scale: Qt.vector3d(100, 100, 100) + geometry: ExamplePointGeometry { } + materials: [ + PrincipledMaterial { + lighting: PrincipledMaterial.NoLighting + cullMode: PrincipledMaterial.NoCulling + baseColor: "yellow" + pointSize: sliderPointSize.value + } + ] + } + + Model { + id: torusModel + visible: false + geometry: TorusMesh { + radius: radiusSlider.value + tubeRadius: tubeRadiusSlider.value + segments: segmentsSlider.value + rings: ringsSlider.value + } + materials: [ + PrincipledMaterial { + id: torusMaterial + baseColor: "#dc322f" + metalness: 0.0 + roughness: 0.1 + } + ] + } + + OrbitCameraController { + origin: originNode + camera: cameraNode + } + } + + Pane { + id: controlsPane + width: window.isLandscape ? implicitWidth : window.width + height: window.isLandscape ? window.height : implicitHeight + ColumnLayout { + GroupBox { + title: "Mode" + ButtonGroup { + id: modeGroup + buttons: [ radioGridGeom, radioCustGeom, radioPointGeom, radioQMLGeom ] + } + ColumnLayout { + RadioButton { + id: radioGridGeom + text: "GridGeometry" + checked: true + } + RadioButton { + id: radioCustGeom + text: "Custom geometry from application (triangle)" + checked: false + } + RadioButton { + id: radioPointGeom + text: "Custom geometry from application (points)" + checked: false + } + RadioButton { + id: radioQMLGeom + text: "Custom geometry from QML" + checked: false + } + } + } + + Pane { + id: gridSettings + visible: false + ColumnLayout { + Button { + text: "+ Y Cells" + onClicked: grid.horizontalLines += 1 + Layout.alignment: Qt.AlignHCenter + + } + RowLayout { + Layout.alignment: Qt.AlignHCenter + Button { + text: "- X Cells" + onClicked: grid.verticalLines -= 1 + } + Button { + text: "+ X Cells" + onClicked: grid.verticalLines += 1 + } + } + Button { + text: "- Y Cells" + onClicked: grid.horizontalLines -= 1 + Layout.alignment: Qt.AlignHCenter + } + + Label { + text: "Line width (if supported)" + } + Slider { + Layout.fillWidth: true + id: sliderLineWidth + from: 1.0 + to: 10.0 + stepSize: 0.5 + value: 1.0 + } + } + } + Pane { + id: triangleSettings + visible: false + ColumnLayout { + CheckBox { + id: cbNorm + text: "provide normals in geometry" + checked: false + } + RowLayout { + enabled: cbNorm.checked + Label { + Layout.fillWidth: true + text: "Normal adjust: " + } + Slider { + id: sliderNorm + + from: 0.0 + to: 1.0 + stepSize: 0.01 + value: 0.0 + } + } + CheckBox { + id: cbTexture + text: "enable base color map" + checked: false + } + CheckBox { + id: cbUV + text: "provide UV in geometry" + checked: false + } + RowLayout { + enabled: cbUV.checked + Label { + Layout.fillWidth: true + text: "UV adjust:" + } + Slider { + id: sliderUV + from: 0.0 + to: 1.0 + stepSize: 0.01 + value: 0.0 + } + } + } + + } + Pane { + id: pointSettings + visible: false + RowLayout { + ColumnLayout { + RowLayout { + Label { + text: "Point size (if supported)" + } + Slider { + id: sliderPointSize + from: 1.0 + to: 16.0 + stepSize: 1.0 + value: 1.0 + } + } + } + } + } + Pane { + id: torusSettings + visible: false + ColumnLayout { + Label { + text: "Radius: (" + radiusSlider.value + ")" + } + Slider { + id: radiusSlider + from: 1.0 + to: 1000.0 + stepSize: 1.0 + value: 200 + } + Label { + text: "Tube Radius: (" + tubeRadiusSlider.value + ")" + } + Slider { + id: tubeRadiusSlider + from: 1.0 + to: 500.0 + stepSize: 1.0 + value: 50 + } + Label { + text: "Rings: (" + ringsSlider.value + ")" + } + Slider { + id: ringsSlider + from: 3 + to: 35 + stepSize: 1.0 + value: 20 + } + Label { + text: "Segments: (" + segmentsSlider.value + ")" + } + Slider { + id: segmentsSlider + from: 3 + to: 35 + stepSize: 1.0 + value: 20 + } + CheckBox { + id: wireFrameCheckbox + text: "Wireframe Mode" + checked: false + onCheckedChanged: { + env.debugSettings.wireframeEnabled = checked + torusMaterial.cullMode = checked ? Material.NoCulling : Material.BackFaceCulling + + + } + } + } + + } + } + states: [ + State { + name: "gridMode" + when: radioGridGeom.checked + PropertyChanges { + gridModel.visible: true + gridSettings.visible: true + env.debugSettings.wireframeEnabled: false + originNode.position: Qt.vector3d(0, 0, 0) + originNode.rotation: Qt.quaternion(1, 0, 0, 0) + cameraNode.z: 600 + + } + }, + State { + name: "triangleMode" + when: radioCustGeom.checked + PropertyChanges { + triangleModel.visible: true + triangleSettings.visible: true + env.debugSettings.wireframeEnabled: false + originNode.position: Qt.vector3d(0, 0, 0) + originNode.rotation: Qt.quaternion(1, 0, 0, 0) + cameraNode.z: 600 + } + }, + State { + name: "pointMode" + when: radioPointGeom.checked + PropertyChanges { + pointModel.visible: true + pointSettings.visible: true + env.debugSettings.wireframeEnabled: false + originNode.position: Qt.vector3d(0, 0, 0) + originNode.rotation: Qt.quaternion(1, 0, 0, 0) + cameraNode.z: 600 + } + }, + State { + name: "qmlMode" + when: radioQMLGeom.checked + PropertyChanges { + torusModel.visible: true + torusSettings.visible: true + directionalLight.eulerRotation: Qt.vector3d(-40, 0, 0) + directionalLight.color: "white" + pointLight.color: "white" + pointLight.position: Qt.vector3d(0, 0, 0) + originNode.position: Qt.vector3d(0, 0, 0) + originNode.eulerRotation: Qt.vector3d(-40, 0, 0) + cameraNode.z: 600 + } + } + ] + } +} diff --git a/examples/quick3d/customgeometry/CustomGeometryExample/TorusMesh.qml b/examples/quick3d/customgeometry/CustomGeometryExample/TorusMesh.qml new file mode 100644 index 00000000..7be60596 --- /dev/null +++ b/examples/quick3d/customgeometry/CustomGeometryExample/TorusMesh.qml @@ -0,0 +1,60 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick3D.Helpers + +ProceduralMesh { + property int rings: 50 + property int segments: 50 + property real radius: 100.0 + property real tubeRadius: 10.0 + property var meshArrays: generateTorus(rings, segments, radius, tubeRadius) + positions: meshArrays.verts + normals: meshArrays.normals + uv0s: meshArrays.uvs + indexes: meshArrays.indices + + function generateTorus(rings: int, segments: int, radius: real, tubeRadius: real) : var { + let verts = [] + let normals = [] + let uvs = [] + let indices = [] + + for (let i = 0; i <= rings; ++i) { + for (let j = 0; j <= segments; ++j) { + const u = i / rings * Math.PI * 2; + const v = j / segments * Math.PI * 2; + + const centerX = radius * Math.cos(u); + const centerZ = radius * Math.sin(u); + + const posX = centerX + tubeRadius * Math.cos(v) * Math.cos(u); + const posY = tubeRadius * Math.sin(v); + const posZ = centerZ + tubeRadius * Math.cos(v) * Math.sin(u); + + verts.push(Qt.vector3d(posX, posY, posZ)); + + const normal = Qt.vector3d(posX - centerX, posY, posZ - centerZ).normalized(); + normals.push(normal); + + uvs.push(Qt.vector2d(i / rings, j / segments)); + } + } + + for (let i = 0; i < rings; ++i) { + for (let j = 0; j < segments; ++j) { + const a = (segments + 1) * i + j; + const b = (segments + 1) * (i + 1) + j; + const c = (segments + 1) * (i + 1) + j + 1; + const d = (segments + 1) * i + j + 1; + + // Generate two triangles for each quad in the mesh + // Adjust order to be counter-clockwise + indices.push(a, d, b); + indices.push(b, d, c); + } + } + return { verts: verts, normals: normals, uvs: uvs, indices: indices } + } +} diff --git a/examples/quick3d/customgeometry/CustomGeometryExample/qmldir b/examples/quick3d/customgeometry/CustomGeometryExample/qmldir new file mode 100644 index 00000000..9d54279f --- /dev/null +++ b/examples/quick3d/customgeometry/CustomGeometryExample/qmldir @@ -0,0 +1,3 @@ +module CustomGeometryExample +Main 1.0 Main.qml +TorusMesh 1.0 TorusMesh.qml diff --git a/examples/quick3d/customgeometry/CustomGeometryExample/qt_logo_rect.png b/examples/quick3d/customgeometry/CustomGeometryExample/qt_logo_rect.png new file mode 100644 index 00000000..129b873d Binary files /dev/null and b/examples/quick3d/customgeometry/CustomGeometryExample/qt_logo_rect.png differ diff --git a/examples/quick3d/customgeometry/customgeometry.pyproject b/examples/quick3d/customgeometry/customgeometry.pyproject index 3e31ac93..d3aeb7d0 100644 --- a/examples/quick3d/customgeometry/customgeometry.pyproject +++ b/examples/quick3d/customgeometry/customgeometry.pyproject @@ -1,3 +1,5 @@ { - "files": ["examplepoint.py", "exampletriangle.py", "main.py", "main.qml", "resources.qrc"] + "files": ["examplepoint.py", "exampletriangle.py", "main.py", + "CustomGeometryExample/Main.qml", "CustomGeometryExample/TorusMesh.qml", + "CustomGeometryExample/qmldir", "CustomGeometryExample/qt_logo_rect.png"] } diff --git a/examples/quick3d/customgeometry/examplepoint.py b/examples/quick3d/customgeometry/examplepoint.py index 3b498422..df5e8f90 100644 --- a/examples/quick3d/customgeometry/examplepoint.py +++ b/examples/quick3d/customgeometry/examplepoint.py @@ -9,7 +9,7 @@ from PySide6.QtGui import QVector3D from PySide6.QtQml import QmlElement from PySide6.QtQuick3D import QQuick3DGeometry -QML_IMPORT_NAME = "ExamplePointGeometry" +QML_IMPORT_NAME = "CustomGeometryExample" QML_IMPORT_MAJOR_VERSION = 1 diff --git a/examples/quick3d/customgeometry/exampletriangle.py b/examples/quick3d/customgeometry/exampletriangle.py index 996a9f85..8cc7a727 100644 --- a/examples/quick3d/customgeometry/exampletriangle.py +++ b/examples/quick3d/customgeometry/exampletriangle.py @@ -8,7 +8,7 @@ from PySide6.QtGui import QVector3D from PySide6.QtQml import QmlElement from PySide6.QtQuick3D import QQuick3DGeometry -QML_IMPORT_NAME = "ExampleTriangleGeometry" +QML_IMPORT_NAME = "CustomGeometryExample" QML_IMPORT_MAJOR_VERSION = 1 diff --git a/examples/quick3d/customgeometry/main.py b/examples/quick3d/customgeometry/main.py index bff6b4a9..169cf17e 100644 --- a/examples/quick3d/customgeometry/main.py +++ b/examples/quick3d/customgeometry/main.py @@ -3,27 +3,24 @@ from __future__ import annotations -import os import sys +from pathlib import Path -from PySide6.QtCore import QUrl from PySide6.QtGui import QGuiApplication, QSurfaceFormat from PySide6.QtQml import QQmlApplicationEngine from PySide6.QtQuick3D import QQuick3D -# Imports to trigger the resources and registration of QML elements -import resources_rc # noqa: F401 from examplepoint import ExamplePointGeometry # noqa: F401 from exampletriangle import ExampleTriangleGeometry # noqa: F401 if __name__ == "__main__": - os.environ["QT_QUICK_CONTROLS_STYLE"] = "Basic" app = QGuiApplication(sys.argv) QSurfaceFormat.setDefaultFormat(QQuick3D.idealSurfaceFormat()) engine = QQmlApplicationEngine() - engine.load(QUrl.fromLocalFile(":/main.qml")) + engine.addImportPath(Path(__file__).parent) + engine.loadFromModule("CustomGeometryExample", "Main") if not engine.rootObjects(): sys.exit(-1) diff --git a/examples/quick3d/customgeometry/main.qml b/examples/quick3d/customgeometry/main.qml deleted file mode 100644 index 45bb4462..00000000 --- a/examples/quick3d/customgeometry/main.qml +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import QtQuick3D -import QtQuick3D.Helpers -import ExamplePointGeometry -import ExampleTriangleGeometry - - -Window { - id: window - width: 1280 - height: 720 - visible: true - color: "#848895" - - View3D { - id: v3d - anchors.fill: parent - camera: camera - - PerspectiveCamera { - id: camera - position: Qt.vector3d(0, 0, 600) - } - - DirectionalLight { - position: Qt.vector3d(-500, 500, -100) - color: Qt.rgba(0.4, 0.2, 0.6, 1.0) - ambientColor: Qt.rgba(0.1, 0.1, 0.1, 1.0) - } - - PointLight { - position: Qt.vector3d(0, 0, 100) - color: Qt.rgba(0.1, 1.0, 0.1, 1.0) - ambientColor: Qt.rgba(0.2, 0.2, 0.2, 1.0) - } - - Model { - visible: radioGridGeom.checked - scale: Qt.vector3d(100, 100, 100) - geometry: GridGeometry { - id: grid - horizontalLines: 20 - verticalLines: 20 - } - materials: [ - DefaultMaterial { - lineWidth: sliderLineWidth.value - } - ] - } - - //! [model triangle] - Model { - visible: radioCustGeom.checked - scale: Qt.vector3d(100, 100, 100) - geometry: ExampleTriangleGeometry { - normals: cbNorm.checked - normalXY: sliderNorm.value - uv: cbUV.checked - uvAdjust: sliderUV.value - } - materials: [ - DefaultMaterial { - Texture { - id: baseColorMap - source: "qt_logo_rect.png" - } - cullMode: DefaultMaterial.NoCulling - diffuseMap: cbTexture.checked ? baseColorMap : null - specularAmount: 0.5 - } - ] - } - //! [model triangle] - - Model { - visible: radioPointGeom.checked - scale: Qt.vector3d(100, 100, 100) - geometry: ExamplePointGeometry { } - materials: [ - DefaultMaterial { - lighting: DefaultMaterial.NoLighting - cullMode: DefaultMaterial.NoCulling - diffuseColor: "yellow" - pointSize: sliderPointSize.value - } - ] - } - } - - WasdController { - controlledObject: camera - } - - ColumnLayout { - Label { - text: "Use WASD and mouse to navigate" - font.bold: true - } - ButtonGroup { - buttons: [ radioGridGeom, radioCustGeom, radioPointGeom ] - } - RadioButton { - id: radioGridGeom - text: "GridGeometry" - checked: true - focusPolicy: Qt.NoFocus - } - RadioButton { - id: radioCustGeom - text: "Custom geometry from application (triangle)" - checked: false - focusPolicy: Qt.NoFocus - } - RadioButton { - id: radioPointGeom - text: "Custom geometry from application (points)" - checked: false - focusPolicy: Qt.NoFocus - } - RowLayout { - visible: radioGridGeom.checked - ColumnLayout { - Button { - text: "More X cells" - onClicked: grid.verticalLines += 1 - focusPolicy: Qt.NoFocus - } - Button { - text: "Fewer X cells" - onClicked: grid.verticalLines -= 1 - focusPolicy: Qt.NoFocus - } - } - ColumnLayout { - Button { - text: "More Y cells" - onClicked: grid.horizontalLines += 1 - focusPolicy: Qt.NoFocus - } - Button { - text: "Fewer Y cells" - onClicked: grid.horizontalLines -= 1 - focusPolicy: Qt.NoFocus - } - } - } - RowLayout { - visible: radioGridGeom.checked - Label { - text: "Line width (if supported)" - } - Slider { - id: sliderLineWidth - from: 1.0 - to: 10.0 - stepSize: 0.5 - value: 1.0 - focusPolicy: Qt.NoFocus - } - } - RowLayout { - visible: radioCustGeom.checked - CheckBox { - id: cbNorm - text: "provide normals in geometry" - checked: false - focusPolicy: Qt.NoFocus - } - RowLayout { - Label { - text: "manual adjust" - } - Slider { - id: sliderNorm - from: 0.0 - to: 1.0 - stepSize: 0.01 - value: 0.0 - focusPolicy: Qt.NoFocus - } - } - } - RowLayout { - visible: radioCustGeom.checked - CheckBox { - id: cbTexture - text: "enable base color map" - checked: false - focusPolicy: Qt.NoFocus - } - CheckBox { - id: cbUV - text: "provide UV in geometry" - checked: false - focusPolicy: Qt.NoFocus - } - RowLayout { - Label { - text: "UV adjust" - } - Slider { - id: sliderUV - from: 0.0 - to: 1.0 - stepSize: 0.01 - value: 0.0 - focusPolicy: Qt.NoFocus - } - } - } - RowLayout { - visible: radioPointGeom.checked - ColumnLayout { - RowLayout { - Label { - text: "Point size (if supported)" - } - Slider { - id: sliderPointSize - from: 1.0 - to: 16.0 - stepSize: 1.0 - value: 1.0 - focusPolicy: Qt.NoFocus - } - } - } - } - TextArea { - id: infoText - readOnly: true - } - } -} diff --git a/examples/quick3d/customgeometry/qt_logo_rect.png b/examples/quick3d/customgeometry/qt_logo_rect.png deleted file mode 100644 index 129b873d..00000000 Binary files a/examples/quick3d/customgeometry/qt_logo_rect.png and /dev/null differ diff --git a/examples/quick3d/customgeometry/resources.qrc b/examples/quick3d/customgeometry/resources.qrc deleted file mode 100644 index dc55e9dd..00000000 --- a/examples/quick3d/customgeometry/resources.qrc +++ /dev/null @@ -1,6 +0,0 @@ - - - main.qml - qt_logo_rect.png - - diff --git a/examples/quick3d/customgeometry/resources_rc.py b/examples/quick3d/customgeometry/resources_rc.py deleted file mode 100644 index 1422353a..00000000 --- a/examples/quick3d/customgeometry/resources_rc.py +++ /dev/null @@ -1,585 +0,0 @@ -# Resource object code (Python 3) -# Created by: object code -# Created by: The Resource Compiler for Qt version 6.2.2 -# WARNING! All changes made in this file will be lost! - -from PySide6 import QtCore - -qt_resource_data = b"\ -\x00\x00\x09C\ -\x00\ -\x00#\x9fx\x9c\xe5ZQs\xdaH\x12~\xe7W\xf4\ -\xf9\xf2`\xef\x11\x19;\x9b\x5c\x8e\xab\xab+\x01\xb2\xad\ -*,\x11I\xd8qmm\xa5\x844\xc0l\x84\x86\x95\ -F&l\xca\xff\xfd\xbaG\x12\x08\x108N\xec\xbd\xba\ -\xba)\x17\xa0\x99\x9e\xee\xaf\xbf\xee\xe9\x99Qr\xfa\xd3\ -3\xb6\x86\xfa\x83\xae\x98/\x13>\x99J8\xee\x9e\xc0\ -y\xeb\xfc\x0c\xbc)\x83\x0f\x12Gfs?^B_\ -\x86Z.\x19K?\x90m\x98J9O\xdb\xa7\xa7\x8b\ -\xc5B\xfb]j\x5c\x9cF<`q\xca\xe3\xc9i\xa1\ -\xd5\x9b\xf2\x14\xc6\xf5\xcd\xaea\xb9F\xbb\xe3\xf6^\xe5pf3\ -\x96\x04\xdc\x8f\xa0\xaf\xcc3\x18\xa6\xfe\x84\xd1X\xd1\x81\ -\x8a\xa7\x22\x0a\x11\x17\xdc\xfb\x11\x0f!X\xcfAS9\ -j\x14\x9a\xf9K\xc8p\xbe\x5c\xa3\x8eI\x8d\x1f\x04\x22\ -\x09\xfd8`\xb0\xe0r\xaa VT\x14\xf3\xc1\x9f$\ -\x8c\xcdX,a\x9e\x88{\x1e\xb2p%NZ\x5c1\ -\x96\x0b?a \x92&\xf8\x91dI\xecK~\xcf\xa2\ -e\x13\xed\xd4\x1aA\x99Y\x8a\xa6\x90p\x1e\xa3\xba\x02\ -\x0e,\x12.%\x8b+\x16GL.\x18\xf6,E\x06\ -~\x1cn\xc5N\x83\x0b${\x15\x9d\x5c\xafR\x15\x13\ -\x19q\xc8%\x17q\x0aHUMT\x95\xf4\xeb\xb5X\ -\xaem\x9c%\x081!-<\xc6X\xce|\x1a,\xf8\ -c9\xe8@Ey\x06\xbe\xacQ[H\xbc\xce\xd22\ -\xc8\x18\xd2\xdd\x18\xea\x9bL\x91\x83\xbbq\xca\xe2\x90%\ -\x15\xca\x8a<\x22\x85Et\x94\xb7(-\xa2H,\xd2\ -va\xf1\xc8a!Oe\xc2G\x99BO|\x90f\ -\x8cG*\xb2\x04cA=#\x1e\xfb\xc9R\xb9\x926\ -\xf3\xe8 \x01\xf4-2Ijf\x22\xe4c\x1e(\x06\ -0\xb6\x18\xe39\xc2\xa0\x18\x85\xeb\x5c\x90S\xa4\x81P\ -\xe5\x18(\x0e\x15\xeaq\x92\xd2\xc4$a\x03\x80\x9f`\ -\x13\x9br\xaa\x00\x15\x88\x90\xc1,K%$\x8cRC\ -\xa9\xf5G\xe2\x9e\x86\x8au\x9ck\x01\x88\x85D\x06\x9a\ -9Y\x11*$=U\xc3q\xb8\x85\x0a\xad\x06\x91\xcf\ -1\xbd\xb5}P\xd0d\x85\x94\x12\x0a\xba\x1af\x01{\ -)4E\xfaS#\x91P\x04\x19\xe5\xbe_F\xee\x14\ -\x83\x22('1A0\x0dpi\xa6\xf5+\x91Z\xd5\ -\x9f\x95\x9b\x16\xe3j>\xa9\x8f\xfd\x19#p\xbbU\x10\ -\x9dX\x8b\xa8\xb0p\x99\x96z)\xab\x95^\x91\xe4\x05\ -e\xc4(\xa3\xd0+\x01,\x0e\xb1\x97\x0a\x00\xe1\x9a\x09\ -\xc9 \xa7L\xa6\x80\xf9\x8b\x19\x1e\x96j\xc68\x9e\x93\ -\x94\x96e\xa3\xc87H\xe7,\xa0l\xc3\xb9\x9c\xd2\xb0\ -\xa8\x05*\xe3\xd2\xb4p\xa7\xac\xbfW\xa6\x0b\xae}\xe1\ -\xdd\xea\x8e\x01\xf8{\xe0\xd87f\xcf\xe8A\xe7\x0e\x07\ -\x0d\xe8\xda\x83;\xc7\xbc\xbc\xf2\xe0\xca\xee\xf7\x0c\xc7\x05\ -\xdd\xeaa\xaf\xe59fg\xe8\xd9\x8e\xab\x96\x89\xee\xe2\ -\xe4#5\xa6[w`|\x1c8\x86\xeb\x82\xed\x80y\ -=\xe8\x9b\xa8\x0f\x0d8\xba\xe5\x99\x86\xdb\x04\xd3\xea\xf6\ -\x87=\xd3\xbal\x02\xea\x00\xcb\xf6T16\xafM\x0f\ -%=\xbb\xa9L\xef\xce\x04\xfb\x02\xae\x0d\xa7{\x85\x8f\ -z\xc7\xec\x9b\xde\x9d2yaz\x16\x99\xbb\xb0\x1dU\ -\x11`\xa0;\x9e\xd9\x1d\xf6u\x07\x06Cg`\xbb\x06\ -\x90\x7f=\xd3\xed\xf6u\xf3\xda\xe8i\x88\x01\xed\x82q\ -cX\x1e\xb8Wz\xbf\xbf\xe9.\xe9\xb1o-\xc3!\ -\x1f\xaa\xeeB\xc7@\xa4z\xa7o\x909\xe5m\xcft\ -\x8c\xaeGn\xad\x7fu\x91D\x04\xd9o\xaa\xca>0\ -\xba&\xfeF^\x0ctJw\xee\x9a\x85Z\xd7\xf80\ -D9\x1c\x84\x9e~\xad_\xa2\x8f\xc7\x8f\xb3\x83A\xea\ -\x0e\x1d\xe3\x9a\xb0#%\xee\xb0\xe3z\xa67\xf4\x0c\xb8\ -\xb4\xed\x9e\xa2\xdd5\x9c\x1b\xdc\x08\xdd\x7fB\xdfv\x15\ -qC\xd7P`z\xba\xa7+\xf3\xa8\x05\x89C\x09\xfc\ -\xdd\x19\xba\xa6\xa2\xd0\xb4<\xc3q\x86\x03\xcf\xb4\xad\x13\ -\x8c\xf9-2\x84Hu\x9c\xddS\x5c\xdb\x16\xf9\x9c\xe7\ -\x8ea;w\xa4\x9a\xf8P\xd1h\xc2\xed\x95\x81\xfd\x0e\ -\xd1\xabX\xd3\x89\x0e\x17\xd9\xebzU14\x89d*\ -\xc7\xd6\xfe\x82e\x5c\xf6\xcdK\xc3\xea\x1a$`\x93\xa2\ -[\xd35N0x\xa6K\x02\xa62\x8e\x19\x81f\x87\ -\xcaw\x0a\x1abS\xe1\xba\xd8L\xe7\xa6\x8a.\x98\x17\ -\xa0\xf7nL\xc2_\xc8c>\xb8f\x91>\x8a\xbe\xee\ -U\xc1\xbevT9N\x18V\xaf\x92,\xb7\xc6<,\x89\xf1$b\ -\xab\xe1F\xe3\x96cIZ\xc0\xd7\x06\xd5\x1c\x1e\xb6\xb1\ -\xc8P\x87z\x5c\xf0PN\xdbpv\xfe\xbe\xa5\x9e\xa7\ -\x8c\x0ay\x1b\xfe~\x9e?\xdf\xf3\x94\x8f\x22\xd6\x06\x99\ -d\xac\x91\xd7\xbeH$m8\xfa\xeb\xfb\x9f\xdf\xbf\xff\ -\xc7\xdb\xa3\x86\xea\xbd\xe1l\xf1\xa6W\xd8(\xed\xdc\xbf\ -\x09W\xcfx\xe6\x99b]\xd4p'\x8f\xdatJ\xc4\ -\xa2\xbe\x1a\x0c\xb0\xc6&~\xbb\xf8n\xac\xfa\x07\xe8;\ -\x16B:\x16t\xd5P\xc5@i\xa4\x98S\xed\x9e\x8b\ -T\xed4mdR\xbb\xc7\xf9\x22y\x13\x1e\xb7\x9a\x80\ -\x7f\xefZ\xad\x93\x95\xf0\xc3\xdaT\x8f'dH\xc4~\ -\xd4W'\xe4\xaf\xdf\xa0\xf2\xf5\xdb\x16\xaaT\x1f\xaf\xcf\ -\xaa\x8a+D\xa1|2\x19\xf9\xc7-\xedg\x04\xa0\x9d\ -\xd3\xc7\xbb&\x9ci[\xe2\xfel\xc4\x91\x93\xee\xf6\xac\ -3\x9aP~l\xcc\xaa\xa0W\x19\xf1\xed\xb8s*\x1e\ -G\x9c[\xac\xb3}\x08\xf1\xf9\xca\xcf\xf3\xfd\x88\xaf\xf1\ -t\x13m\x81]%[\xe2\x87\x5c\x5c&<\xa4,\xd6\ -\x82)\x0b>\xb3pC4\x0d|\x12\xac:u\xd6\xca\ -}\xaaqlR,\x866\x94J\xe9i\xcb:5J\ -\xa8\x09\x8a\xec\x0c`\xee\xf2?\xe8,\x8b\xd9\x11\xb3\xb4\ -\x0d\xc5\xfa\xd8\x80\xcf\x12<\xf7\xec\x93x\xd8xZ\x1d\ -^\xda\xf0\xcb\x8e\xa2\x1e\x1b\xfbY$\xaf\x0b\x99\x1a\x9c\ -\xd4\x224s\x9b/\xdf\x14\xef;,\xe9\x97\x1d\x1a^\ -\x80\x8a\xd5\xba\x1f\xc1\xafuQ9=\xfd\x0b\xfc2S\ -\xa1\x91E!\xf9\xf5\x09!\xeb\xe2\x11\xf1\xd9C\xb6\xa7\ -\xb0\xd5\xb0\x12\xd3\x05\x85(\x0dF\x16\xfe\xacE\xb1\x96\ -\xfbxW\x12\xa7\x84\xeb9\xcb\xeeI\xdb\xf0f\xaf\xae\ -\xec^\x0f\x7fC\xb7K](\xba\xab\xe9\xd9c\xef\xb1\ -/2\xc3\xe3b\xfd(5\xca\xe4\x91\x9f2\xb5:\xaf\ -\xfd\xf9^\xc1\xfc\xb6\x81\x05\xfdw\xf9)\x12\x13\xf1\x89\ -\xea\xa06\x8f'G\xb5S\x1ej{\x83,\x8a(=\ -\xda\xdb\xf85\x0b\x93\x22\xc2T\x9d\xd4\xce\xc3\x1b\xd5\x18\ -\x0f\xce\x88\x8fh.\xbc*\xb9\x86\x7fox\x00m\x88\ -QU\xad\x1e\xda%\xb2\xc8O\xf4\x99\xc8b\x8cEK\ -{\xfb\x84\xf4/\x7f\xd5f\xff\x13\xd2\x7f\xb5/\xbfD\ -\xfeol\xfa\xf0\xf5%\xea\x09n\x1f\x18\xa8\xba\x18\xf6\ -\x8b\xb1\x97\x08~\xb1\x7f\x1c-\x19\xdd\x06\xeb\xb3nN\ -\xce\xbb\xfc\x0fV.\xb3A\xd9\xf1\xe4Z\x97\x7f\xaa\xaf\ -[?\x0d\x8b3Z\x84\x17\xc25-A\xd9\x19\xda\xa3\ -\xdf\x18\xbd\x06\xab\x9c2\x8a\xc9\x08;\x9b\xc5\xf9Q\xae\ -2\xb5\xef\x8fv\xb2Db^\xa3\x83C\xbc\x15\xde\xea\ -nO\xdd\x7f1Q\xe9\xe5\x86\xc0\xfb\xe5=\x9f a\ -\x9b\x8e\x8f\x11\x816\x12QX9ym\xfa\xd5\xc9\xa4\ -\x14\xf1e\x22\xb2\xf9\x96\xb9\x91\x1a\xa1L\xd8\xdcH\x9b\ -\x9bE\xba\xb9\x95\xb4\xb5K\xc2!\x91\xdcT\xcd\xd9k\ -C}\x9d\xcb\xd5\xedv\xd3\xc1b\x85l\xb9\x97\xbb\x1e\ -d\xe9@Dp\xa5\ -\xb3u\xe0h\x0c\xdd.WZ~\x1d\xf7\xe7\xf3\xa8x\ -\xc9\x03\xc7e\x158\xd9\x83}\x8c\x0b\xee\x05\xc1\xafB\ -\xf3}\xe8\xd5\xa2I_\x04\xbbX\xec$>\xb5'\x1c\ -\xe0\xf6,\x9f\xb2\xd5\x12\xb3\xc5\xc0\xb5\xc0\x1d\xf0#\x04\ -X7\xd2\xfa\xb2!\xe2.z\xa3\xdc\xa5S\x9d\xb6q\ -B\x83\xbf\xfd\x0b\xcej\xa7=F\xc4.![\xb0\x0f\ -\xe3\xbe`\x0b\xac6?\x00\xfc\xf5\xf3\x02\x7fx\x91\xc0\ -\xdc=\xc5\xbf\xad\xe3\xf5\x7f94?\x04\xfd\xc5\x82\xf3\ -\xac\xcb\xafn\x9b\xaa\xf0@\xae\xe4\xef\x06\xe0\x98\x8f!\ -\xcd\xe6\xf4\x8e\x81\x85[\xc5d\x13\xaa\xabv\xe6=\xb7\ -\xaa\xad+\xca\x8e\x0c\x95\xaf6\xdd\x16w!\x09\xeco\ -\xd5\x0c\xa4\x92\xcd\xf33A\xddqO\x9d\x0b\xea5~\ -K$\xbe\x9f\xf7\x83\x97\xa0.\xf5u\xc4\x97=,\xe5\ -\x17\x97}Q)\xdes\x97\xf7\x1czA?\xa9\xdd\x5c\ -\xa9\x1d(\xf4O\xe7\xe0\x90\xf7\xd4\xf6\xa5S\x05\xfc\xcc\ -\x8f3<{\xfa\xea\xb6\xb4\x0bww\xc1\xeeM'j\ -\xeb\x94\xaa%Ly\xa82\xaa.q\x14*Q\x9f\x1b\ -\xd4\xaa\x89\xd5\xaa_\xccEv\xed\xd3\xfeg\xaf\xf5\x1f\ -\xcb\xb9\xe2\xde\xb5/\xedX\xec\xa3%u\x17\xcb_\x11\ -\xe1mc\xfe'$\xdc\xa3\xb8\x877\x8f\xad\x94\xe1\xcd\ -\xff\xd6\x22A\xbc\xcf\xbe@jXR\xbe\xfd\x1f-\x8f\ -\xc3\x17\xf3GN<\x87BJ\xedPX\xa9\x15\xa1U\ -\x18 E\xde\x0en\xa9\xf5\xae\x97\xed`\xc8\xa9\xad\xc3\ -\xbe\xba!\xef\x95\xdd\xbf\xe5\xae\xa0S\x16\xbc; \xb0\ -N\x85Cj\x0el\xc5+,\xdf\x98\x11\xd4v\xa9y\ -\xd8\x01\x00|\xb0\x03\x00\xf8`\x07\ -\x00\xf0\xc1\x0e\x00\xe0\x83\x1d\x00\xc0\x07;\x00\x80\x0fv\ -\x00\x00\x1f\xec\x00\x00>\xd8\x01\x00|\xb0\x03\x00\xf8`\ -\x07\x00\xf0\xc1\x0e\x00\xe0\x83\x1d\x00\xc0\x07;\x00\x80\x0f\ -v\x00\x00\x1f\xec\x00\x00>\xd8\x01\x00|\xb0\x03\x00\xf8\ -`\x07\x00\xf0\xc1\x0e\x00\xe0\x83\x1d\x00\xc0\x07;\x00\x80\ -\x0fv\x00\x00\x1f\xec\x00\x00>\xd8\x01\x00|\xb0\x03\x00\ -\xf8`\x07\x00\xf0\xc1\x0e\x00\xe0\x83\x1d\x00\xc0\x07;\x00\ -\x80\x0fv\x00\x00\x1f\xec\x00\x00>\xd8\x01\x00|\xb0\x03\ -\x00\xf8`\x07\x00\xf0\xc1\x0e\x00\xe0\x83\x1d\x00\xc0\x07;\ -\x00\x80\x0fv\x00\x00\x1f\xec\x00\x00>\xd8\x01\x00|\xb0\ -\x03\x00\xf8`\x07\x00\xf0\xc1\x0e\x00\xe0\x83\x1d\x00\xc0\x07\ -;\x00\x80\x0fv\x00\x00\x1f\xec\x00\x00>\xd8\x01\x00|\ -\xb0\x03\x00\xf8`\x07\x00\xf0\xc1\x0e\x00\xe0\x83\x1d\x00\xc0\ -\x07;\x00\x80\x0fv\x00\x00\x1f\xec\x00\x00>\xd8\x01\x00\ -|\xb0\x03\x00\xf8`\x07\x00\xf0\xc1\x0e\x00\xe0\x83\x1d\x00\ -\xc0\x07;\x00\x80\x0fv\x00\x00\x1f\xec\x00\x00>\xd8\x01\ -\x00|\xb0\x03\x00\xf8`\x07\x00\xf0\xc1\x0e\x00\xe0\x83\x1d\ -\x00\xc0\x07;\x00\x80\x0fv\x00\x00\x1f\xec\x00\x00>\xd8\ -\x01\x00|\xb0\x03\x00\xf8`\x07\x00\xf0\xc1\x0e\x00\xe0\x83\ -\x1d\x00\xc0\x07;\x00\x80\x0fv\x00\x00\x1f\xec\x00\x00>\ -\xd8\x01\x00|\xb0\x03\x00\xf8`\x07\x00\xf0\xc1\x0e\x00\xe0\ -\x83\x1d\x00\xc0\x07;\x00\x80\x0fv\x00\x00\x1f\xec\x00\x00\ ->\xd8a}i\xaf;\x02\xab\x81\xffgL\xb0\xc3:\ -\xc2\xa9\x1c\x1d\xfe\xa51\xc1\x0e\xebE\xf5<\xde\xd9\xb7\ -g{_\x1b\xac\x92\x1d}m\xb5\x7f)\x82\x88\x06v\ -X/\xda_\xee\x7fcG\xdf\x9e\xd7n\xbf\xb5o\xa8\ -{\xef\xd0Ah\x19\xfd\x03;\x07\xbb^\xee\xef@\x0d\ -q\xc1\x0e\xeb\x82\xa9\xa1\xad}\xe0\x9d\xdfg\xef<%\ -V\x1dw\x1f\x8fv\xfc\xf1\xael\xab\x7fl\xdd\x7f\x1b\ -Z\x04;\xac=\x8b\xb3\x86[3\xfd:\xb3\xe7\x9f\xce\ -/<]\x80\xd6\xa8,\xcc\xeb\x7f88[zc\x00\ -;D\x06;\xac1\x89\x1av\xf6\xed\xd9u\xbb\xf3\xc6\ -\xf4\xcdD\x0d\x0b\xff\xd3Vg\xb9\xb6D\x0ba\xff\xc0\ -\xa1\xd9a\xec\x10\x1d\xec\xb0fhI\x9c\xaa\xa1\xbf\xfd\ -\x95\xfe\x8e\x9f\xa7\xae\xdb\x99\x1d^\x03\xd3S\x9dXq\ -`\x87\xe2\xc0\x0ekFU\x0d\xda^\x9e\xbcj\xa7\xb5\ -I\x015\xac&\xb0Cq`\x87\xb5!\x91\x82M\x1f\ -.N^\xb1s:\xa8\x01;\xac&\xb0Cq`\x87\ -5\xa0\xfa6\xdb\xce\xbe=\xdf\x97\x7f\xd4\xa9\x1c\x8c\x80\ -\x1aV\x1f\xd8\xa18\xb0C\xd1T\xd5\xb0\xbd\xaf\xedl\ -\xf9\x82\xcec\xbc\x107\xb0Cq`\x87BI\xd4\xa0\ -\xf3u[\xdf\xab\xa7\xc6\xcf\xe9$\x0eR@\x0d\xb1\x02\ -;\x14\x07v(\x8eE5\x9c\x1c;\xad3\x185\x14\ -\x11\xd8\xa18\xb0CA\x045\xec\xfe\xea~\xaf\x9d\xc7\ -A\x0d\xd8!b`\x87\xe2\xc0\x0eE\xf1r\x7f\xc7\x8b\ -}\xbb\xbf\xf8\xeb[;\x89QCA\x81\x1d\x8a\x03;\ -\x14A\xbb\xd4\xf0\xaf[\xff91\xfau\x90\x82mQ\ -C\xf4\xc0\x0e\xc5\x81\x1d\xe2\xa2\x05EU\x0d\x1f\x8f~\ -YY\xa8\xe8\xc4\xcd\x0a\x82\x88\x1e\xd8\xa18\xb0CD\ -\xecZC\xa2\x86\x0f\xee}\xfaxaNgm\xa1j\ -P\xb3\xa1\xfd\x1c\xf35r\xc7\x85\x95O\x1b\xd8\x0a\x81\ -\x1d\x8a\x03;\xc4\xa2z\x19\xf2\xc5\xbe\xdd\xc7F>\x99\ -\x9d\x7f\x94\x9c\xb8\x85\xa5bHr\xa1\xf4\xb0\x0cYN\ -$\xe5\x95P\x19M\xd8\xce\xe6\x0d\xecP\x1c\xd8!\x0a\ -\x89\x1a\xc4\xf6\xbe\xb6\xee\xe1\xe3\xd3\xf33\xe1\xacU\xc4\ -M\xbf\x90\xd5\x89\x14\xe4\x9fZh\x15\xf3wejl\ -n\xfc\xee\xe3\xd1\xe1\xc7#\xca\x96\xc1\xd9\x92\xb6\xda\xd7\ -\x11\x1d\xd7\xa3s\x0bO\xaa\xa5\xd3\xe1\x89\xd0\x9a\x1d\xdc\ -\x8c\x81\x1d\x8a\x03;DcG_[\xe7`\x97\x920\ -9ekY\x171\xf1Bk\xda\x06\xf5LT&\x7f\ -\x9d\xfe\xedl\xf9\xc2\xc7\xa3_\x1e(\x1d\xed\x18\xdc\xaf\ -\xa5\x8d$\xa5\xd5\xcd?o\xbd\xa2\xed\xf6\xbeW\x950\ -\xca\x9c\xae\xd2\x91\x0f\xef}\xfe]\xf9\x87\x1b\xd37%\ -\x0b\xab\x9e\x15\x84\xed,3\xb2\xb5\x9a\x10J\x16\x17\xd8\ -\xa18\xb0C4\x94\x93\x87\x86{\xc2\xf9\x1a7+B\ -kaGS\x83\xaf\xee\xf7*\xe7w\xf6\xedy\xe1\xd6\ -KZ\xd1h\x00\xda\xd7H$\x88Wjh_Gv\ -\xf6\xb7\xebQ\x95\x912\xb4\xb3o\xa8\xfb\xb3?\xbf\xb1\ -{\xcf(B\x1a\x1bv\xf0\x99\xb1\x9c\x92\xcbo\xad\xe5\ -\xc0\x0e\xc5\x81\x1d\xa2\xa1\xac;8\xfc\x7f:SW\x94\ -c\xcb\x09k-4;\xfe\xa4\xac\x99\xc2\xeb\x03ok\ -j\xb0-\x9d\x1a\xec\xba\xdd\x99\x8a \xb9\xf0\x91\x1b\x95\ -a\x0f\x995lr!M\xa8\xd6\xb1\x91O\xb4\xf4\xc8\ -6n\xfb\xb6\xd3$\xac\x8c\xb6\xe5\xca\x84f\x22\x1aR\ -\x16[\xc8\x842i\x8d\xa2\x02;\x14\x07v\x88F\xb0\ -\x83\x9d\xaf\xb1\x22\x97c?M]W\x1aH\x0a\xca\x84\ -05\x08\x17>\x96A\xf5\xea\xa9\xeaj\xa2\xa1v^\ -\xbb\xfd\x96}sT\x11zyfJ[\x01=SM\ -C\xd4\x82T\x15h\x1fxG\x8d\xcb_\xd9wm\x8a\ -\x0b\xecP\x1c\xd8!\x1aE\xd8\xc1R+$\xd8\xd9\xf2\ -\x05\xf5b9PK\x83\xe5{!\x8f5\xa2E\x87\x1c\ -\xa1E\x8a\x0d;\xd7c\xa3\xb0\x02\xaa\x12T\x15\xd0\x90\ -41\xd1\xac\x04;lv\xb0C4\x8a\xb3\x835(\ -5(\x0f\x95\xcc\xb5\x1e\xeb\xbdP=bi\x1f\xc8=\ -\x9aa\xb1\xbc\x16)\xd9\x0f}/\xdf\x0e\x9d\x83]v\ -\xb1#\xa0\x89\xc3\x8b}\xbb{\xee\x9e\xc0\x0e\x9b\x1d\xec\ -\x10\x8d\xe8v\xa8f`\xfa\xb6\xe5\xb5\x87\xbf(\x99k\ -j\xf0\xf3+\xc14;P\xb2\ -\x95+\x13\xd9\xc25;\xe4\x87\xb4R\xec\xc2\x04v\xd8\ -\xf8`\x87hD\xb4\x83\xb6cs\xe3\xed\x03\xef\xe4R\ -1=\xf5\x93%\xbd\xdd\xd6\xa9\xe5\x8eB\xc2\x8f?)\ -\xa7\x8b\x97\xc5.\x0cK\xe0\xbb\x8fG\xb3\x85\xb1\xc3\xf3\ -\x06v\x88F,;\xd8\x07\xa8\xfbg\x06\xb4\xc8\xcf\xa5\ -\xaeN}%\xa7\xe6\x14\x96\xb7vC\xfa\xb4\xd2\x8a#\ -$\xbc\x1cT\xff\xb6\x88\x12\xf8_\xb7\xfec\x0e\xca\xda\ -\xc1\x0a\xeb\xd1\x80\x95_Z\xbdz$[,\x8b\xec\xb6\ -\xbd\xef\xd5\x0f\xef}\x8e\x1d68\xd8!\x1a\xd1\xec\x90\ -V\xbf\xbe\xfaW]\xab\ -[Y\xa8(K5\xf2z;(\x8d\xb3\x1f\xa9\xd0V\ -\x9d\xbe?\xf2QW\xe9\xc8\x81\xd2\xd1\x80\x14\x96\x1b\xa4\ -\x90\x1a\xda\x07\xf6f\x8be\xd1\x7fi\xdfP\xb7\x1a\xb7\ -;\xe8c\x87\x0d\x0bv\x88F\x14;(U\xac\xfa\xc9\ -\xb1\xd3j0\xd7\x85RQ/\xdd\x9a\x93[\xc9\xd5\xe4\ -\x95\xd5\xd5\xf6\x8b\xbf\xbe\x95\x08\xe4\x9dlG\xca1M\ -\xfe\xf5P(f\xdb\xbf+S\x9a\xb6hkh_9\ -ioR\x064H)L\x83,W&r\xe5\x03:\ -8=?\x93m\xbc\xe5\xc0\x0e\xc5\x81\x1d\xa2\x11k\xee\ -PYH\xe6\x0e\x9f\xfd\xf9\xcd\xb6\xea7,\x02\xd5\xb7\ -3O\x8c~m\xc5V\x93W\xa1\xee\xa9\xf1sJf\ -\xcf\x0em\xca\xf0\x90\xc0\x8d\xfa\xea\x1c\xecRNf\xeb\ -\x9a\xc2l\x90\xcf\x8c\xd5<\x05\x0b\xecP\x1c\xd8!\x1a\ -\xb1\xe6\x0e\xf6\xf5\x84\x0f\xee}\xaa\x1c\xcb%\xad\xd9\xc1\ ->\xec\xb0\x9co14\x89\x90\x96g\xcb\x17\xea;2\ -;h\x1da\xcf\xc5\x0ak\x9bC\x86r\xbfgav\ -\xb0\xcfJ\xab\x85l\x95\x1c\xa1\xf1\x96\x03;\x14\x07v\ -\x88F<;$s\x87c#\x9f\xd4\x92vqUo\ -v\xf8\xea~\xaf\x0a\x84\xef)\xb4\x16!9\xbf/\xff\ -\xa8\x8e\xd4r\xe8\xc5P\xa6\xe9\xe9d\xed\x90\x0d;\xa2\ -G\x9b\xd8\xa1Q\xdd\xb8\x81\x1d\x8a\x03;D#\x96\x1d\ -\xec\xaa\xa4^\xb7\xbd\x97\xf4$\xf1\xec\x8b\xdb\xb1\xecp\ -q\xf2J#;\x1c(\x1d\xb5\x19J}Gv\x04;\ -lm\xb0C4\xa2\xd9!\xad\xde\xc0\x0e\xc9\x1b\x8d\xf6\ -V\xc2\xeaW\x16\x96\xb7\x97'\xaf6\xb2\xc3\xfe\xd2a\ -\xec\xf0<\x83\x1d\xa2\x11\xd7\x0e\xdd\xc3\xc7];l\xeb\ -{\xd5n\xcdb\x0b\x90\x96C\x1dY\xde\xfe4u]\ -\xab\x15\xd7\x0e\xfb\x86\xba\xb1\xc3\xf3\x0cv\x88\xc6\xda\xcc\ -\x1d\xd6\xd2\x0e\xcc\x1d\x9es\xb0C4\xd6\xcc\x0e\xb5\x95\ -E\x1c;\x5c\x99\xba\xd6\xc8\x0e\xe9u\x87\xa4\x97\xfa\x0c\ -\xb7#\xd8ak\x83\x1d\xa2\x11\xd7\x0e\xee{\x16\xfa3\ -{\xdda5\x89\xa7\xbaV\xbd\xf1u\x87\xb6C\xc3=\ -\x8d2\xdc\x8e\xe8Q\xec\xb0\x85\xc1\x0e\xd1\x88e\x07\x9b\ -\xcc\xf7\xdc=Q?wP\xe2e\xdf\xd1Lk\xb4\x18\ -\xea\xc8\xf2\xf6\xfc\xc4\xa5Fv\xe8\x1e>no\xa0\xd4\ -g\xb8\x1d\xc1\x0e[\x1b\xec\x10\x8d(vPX\xda\x7f\ -<\xfae\xfa\x11\xc6%]\x98\x1d>\xfb\xf3\x1b+\xb6\ -\x9a\xc4\x0bu\xbf+\xff\xd0\xe8\xb3\x92\x9a\xbf4\xcap\ -\xec\xf0<\x80\x1d\xa2\x11we\xa1\x09B\xfa\xf5\x87l\ -\x17\xd5ORK\x1c*\xa0b\xabI\xbcP\xf7\xe4\xd8\ -\xe9FvP\x86[\xb1\xfa\x8e\xec\x88\xc6\x80\x1d\xb60\ -\xd8!\x1a\xb1\xe6\x0e\xe1\x86\xd1\xb9\xac\x13\xcaa%\x9e\ -^\xd2\x95r\x86Ui!Bz+\x8d\xeb4\xb4\xe4\ -\xf2g(\x9c\x8dP\x1d;la\xb0C4\xa2\xd9!\ -\xad~~\xe2R\xda\xac\xf3\x0d\xeeC\xc3=\xe1\xc7&\ -\xd2\x1a\xad\x84\xd5\xd5\xf2\xe4\xfd\x91\x8f4r\xcf\x0e\xbb\ -\xbf/\xff\x98-\x9c\x0d\xec\xf0<\x80\x1d\xa2\x11\xd7\x0e\ -7\xa6o&?f\xb14\xf1t\xea\xab\x97\xbdC\x07\ -\xc7\xe6\xc6\x93\x92\xabH\x1e\ -\xfd\xd2K\xdd\xe4\xc6\xb3\xdd\x99_\xc1Mk\xac8\xac\ -b\xcf\xdd\x13\x8d\xba\xc8\xde3\xba\xbe\x17;\xa2g\xda\ -\xe8\xdeP\x1a\xbc}p\xa3\xe5\x11.3\xb0Cq`\ -\x87hD\x9c;XF]\x9c\xbc\x92N\xf8\xf3s~\ -\xa1\x1cX\xfd\xefh\xa6w\xa0\xdf\x9b\xcb\xed0=\xb1\ -7,\x1a5n\x075\xc7\xa9\xbf\xa8\xa9\xeaZ\xfbH\ -.\xd8a\xb3\x83\x1d\xa2\x11}\xee\xa0e\xbf\x9a\xad\xff\ -\xb1\x09\xcb\xde\x93c\xa7\xad\x98\xb6\xcb\xcf@+i+\ -\x97\xcb\x93W\x95Ku\xedWW\x16\xfd3\x03I\xc9\ -\x06\xcf%tm7\xbc_Z=\xb1\xc3\xb1H7\xa4\ -\x7ff`\x87\xe2\xc0\x0e\xd1\x88n\x07\xb5\xa3W\xe6\xdc\ -\x9b\x9a5\x92\x1f\xd1\x0f\xd7\x05l\xdb<\x0fC\x81\xb0\ -sh\xb8\xa7\xfe\xe3\x98\xfaSOd\xdfP\xf7\xf8\x93\ -r\xb6p.BSz\xca\xf5vP\xb3\xd9\xdf\x01v\ -[\x88\x15\xd8\xa18\xb0C4\xa2\xdbAqj\xfc\x5c\ --\x81\xb3\xaf\xf0\xc9\xbe\xba\xb3\x8fT+B\xf9Fy\ -\x18RT[\x1b\x9e\x16\x0e\xa9w\xac\xd9\xd0xub\ -b_\xf4\xb2ZV1\x17\xa1\xb5\xf7G>\xca]\xb9\ -\xd0h\x95\xa5o\xdey/\xfb\x96\x87\xdbH\x94\xc0\x0e\ -\xc5\x81\x1d\xa2\x11\xd7\x0e\x96Nw\x1f\x8f\xaaY\xbd\x1a\ -\xe7\xfaJiW2|W\xfe\xc1\xaaT\x16\xe6\xc3\x0f\ -\xe7eS1\x1c\x11\x1a\x98\xfd\xf9\xeb\xf4o\x9az\xc8\ -\x0eK\xbd\x93L\x1c\xb4\xd0\xd0C\xcf\xfc\x9d\xce\xd0\xc5\ -\x17\x7f}\x9b\x8e\xd0Z\xa8\xa2\x01\xeb\xa0]\x19\xa9,\ -T\xd4\xce\x22O5\xce\xf9d\xb4\xb5\xc1\x84\xa6Z\x0b\ -\xb5\xa3-v(\x02\xec\x10\x8d\x88vPX\xce(\xb5\ -z\xee\x9eH\x7f\xd8\x22\xf7\x99\xa5d\xdf.\x19\xf4>\ -8c\xef_(\xd4u}6\xa6\xe2\xa8\xde*FG\ -.N^Q\x22\xd5RzI\x9b\xe9\xa2`\xb7\xa6\x03\ -\xea=`\x15s\x11\x8e\x9f-_\xd8\xee,O\x92K\ -\x0f'\xd2\x9b\xd6[\xb1F\xd1\xa8\xfd\xe5\x87\x9e\x91\xb6\ -\xd8\xa1\x08\xb0C4\x22\xda!\x9b\x997\xa6o*\xfd\ -4\xe1\xcfu\x17\x12[yx\xa0t\xf4\xf2\xe4\xd5\xd1\ -\xb9\xfbV\xdd\x8d\x89\xca\xa4\x9a\xfa\xf0\xde\xe7Z\xaa\xd4\ -\xcd\x1a\xaa\xfb:\xae\x8e4\xb3Py{\x16\x8d\xb27\ -;<\x0d\xc0\x13Mr\x87\x08u\xa7\x81i&\xd2?\ -3\xa0fU\xf8\xe7\xa9\xebW\xa6\xae\x9d\x9f\xb8\xa4#\ -vq\xb4Q\x17\xcb\x0c\xecP\x1c\xd8!\x1aq\xe7\x0e\ -\x0aK\x1b\xbd\xfc\xda}\xa2RAd3P$\x7f\xea\ -\xb8\x12^\xbdk\xa9\xff\xc1\xbdO5\x95\xf8\xbe\xfc\xa3\ -rR\xa9hyxj\xfc\x9c^\xc6\xf7\x97\x0e\xeb%\ -]\xedh[\x97\xccU\xd4\x88\xf2Y\x9d6W\x83\x22\ -\xd8AJr\x9b2A\xa8;\x8dMK\x15\xb1\xebv\ -g\xed)\xb4\xbfp\xeb%\xcd\x89\xf8\x1d\xcd\x0d\x0ev\ -\x88F\x5c;X\xceXS\xbf\xcf\xdeQv\xd5N\xfd\ -zA$3y=\xa4\x01X6\xda\x14@\xd9(\xf4\ -h8\xae\xf2i~:\x8d\xa8\xba^\xea\x95`\xe1\xad\ -\x8a\xb0m\x14\xf6\xe8\xec\xfc#=kuQ\x97\x96I\ -\x17\xeaNc\xd3\x0aHCJ\xd7A\xd5\x83*/;\ -\xf0\x1b\xdc\x1b\x1c\xec\x10\x8d\xb8vP(m\x84\xb5\xa6\ -\xe9@\xfa\xd6@\x92\xc6u\xb9]=\xa2\x87j\xf3\x82\ -\xe4\x92DH\xc8\xda\xf1\xe0\x85\x5c\xf5D.i^u\ -\xd8g\x1c,]\x9b'\xad\x8d\xcd\xca|W\xfeA\xf6\ -1\x13\xe5Z\xb6#\xe9\x00\x12\xec\xb8:\xd2Zi\xf5\ -\x1f\xf7\xb4\xc0\x0e\xc5\x81\x1d\xa2Q\x84\x1dlk;'\ -\xc7N\xdb\xeb\x7f\x9a\xe7\xb9<4\xdc\x83\x81\xfaG\x93\ -\xecU:\xd9+\xffOS\xd7Cw\xd6\xe33\xc3\x8a\ -\x8d\xcd\x8d\xb7\x0f\xbc\xa3\xcc\xac-\x1cr\xbd\xe4\xb1\x1e\ -\xbbJG&*\x93\xa1\x91\x96\x03;\x14\x07v\x88F\ -t;(,s\xd2tM\xa2\xf7\xc1\x19\xcd\x08\xd4Q\ -\x83\x05Bk\xb4w\x0ev\xdd\x98\xbeY\xeb\xa8\xdac\ -\xdaa\xb3\xb0\xc2V\xf2\xe2\xe4\x95m}\xbb\xd5ZM\ -\x10\xcd\x06\xa6\x04V\x1a\xef\x1d:X\xaeLX;i\ -{-\x06v(\x0e\xec\x10\x8d\x22\xec\xa0\xb0\xe4\x09y\ -\xf8\xf3\xd4\xf57\xef\xbc\xf7\xcf[\xaf\xa8\xc7\x06\x99P\ -\x9f\x99\xd9#K\x1e\xb5\x97\xf1\xb3\xe5\x0bj\xb9R\xbb\ -\x8d\xed\x8a\xd25TI\xdf\xdal\xd3\xecFm6O\ -Q\xcd}\xb4\xe4\xe9\x18\xdc\xdf\xe4\x1b\xe2\xcb\x0f\xecP\ -\x1c\xd8!\x1a\x05\xd9A\x112\xd0v\xf4\x92{j\xfc\ -\xdc\xeb\x03o+\x15\x95\x0f\xd9\x0b~\x96\x99\xf5d\xc6\ -\x99\xb7\x83\x1ay\x7f\xe4\xa3\xec\x05\xc2\xe5\xa7k\xae|\ -\xff\xcc\xc0\xb1\x91O\xb6'o\xa0$\xef\xa1\xecH\xd8\ -\x93\xa1M\x07\x0du*\xc7e\xaf\x80\xb6\x1c\xd8\xa18\ -\xb0C4\x0a\xb5C\xc8\xc3\xd0\xb8\x16\xed\x9a\xcfw\x0f\ -\x1f\xdfu\xbbS)a+\x0ee\x9d\xb0\xe4\xacaG\ -\x92D\xad\x0d5\x08\xa2*\x94\x17\xfbv\x9f\x9f\xb8\xa4\ -6\xd5\xf8Js5\x0c\xcc\xfe\xd4\x04dt\xee\xfe\xe5\ -\xc9\xab'F\xbf\xd6\xd8\xbaJGd\x01\xad\x5c\xf6\x0d\ -u\xcbA\x9f\xfd\xf9\x8d\xa6\x18\xd7\x1e\xfe\xa22\xd3\xf3\ -3\xf6\x5cV\xdac.\xb0Cq`\x87h(\x03\x0b\ -\xb2\x83E\xc8\xc3\x5c:\xfd]\x99\xba1}\xb3\xf7\xc1\ -\x99\x0f\xef}~\xa0tT\xebyKHC\xfb:\xd2\ -s\xf7\xc4\xa1\xe1\x9et\x9c\xa6\x86EA(\x9d\x94T\ -*\xd9\xf2\xd7\x22\x1a\x0d\xcc\xa2\xd1q\x8b&\x0f-3\ -\xb0Cq`\x87h\x14m\x07E\xc84\xdbi\x94Z\ -z\x01\xd7JA/\xce\xc2>q\xa4\xd0\xbe\x14\x90\x99\ -A\x04\xaa\xdf\xbc\xfa\xe2\xafo\xadd\x93\x96\x1b\x85\x95\ -\xb7\x8a\xcd\xabg\xcbX1\xdb\xb6\x1c\xf6\xdf\x1e\x9c-\ -a\x87\xe8`\x87h\xac\x81\x1d,BR\xe5P\xbf\xc2\ -\xf6\xad\xa4\x85\xfe\xb4\xef;h%\xa2A\xa6\x13\x870\ -w\xa8\x92&U{\xcbw\x94\xb1\xb0*\xa1zsB\ -\xf9\x96\xc3\xda\x09v\xe8\xf8\x03;D\x06;Dc\xcd\ -\xec\xe0F\xc87\x17{H\x03\xd3\x08\xd3OUi\xc0\ -YA$\xfb:n\xe3\x0f\xed\xd8\xfe\xc6\x8c0<\xfb\ -oke\xd1>\xb0w'v\x88\x0av\x88\xc6\xfa\xda\ -\xa1y\x84l\xff}\xf6N\x93\x17Xe\x97\xe6\x17*\ -fOa#\x0b\xc2\xc6\x16\xb6Z\x16mK\xdeL\xb5\ -\x8f\x81@\x1c\xb0C46\x85\x1d\xb4\xfd\xec\xcfo^\ -\xec\xdb\xed~\xaeQO\xa1s\xb0+\xfb\x01g\xdbn\ -\xb4\xc8\x8d\xad\xf7\xc1\x19\xa9\xa1\xf6,\xf2O\x0aZ\x06\ -;Dc\x83\xdbA[\xfb\xc6\xf4\xe8\xdc}\xfb\xe0\xb3\ -]k\xa8\x8d?\xd9\xd1\x11=\x8b\x93c\xa7\xd3\xc2\xf3\ -\xaa\xb5\x01\xed\x10\xd4`;\xa7\xc6\xcfIv\xd9g\x01\ -\xb1\xc0\x0e\xd1\xd8\xf8s\x07ac;[\xbe\xe0\xcd\xc3\ -\x93w7\xb5\xb8\xd0\x02>\xfc\xd6VZ{\x03\x85\x19\ -\xc1\x9e\x8bv\xec\x89\x84\xf1g\x9e\x0bD\x00;Dc\ -#\xdbA\x11\xf2J\xdb\xd9\xf9G]\xa5#\xe9][\ -\x96\xac/\x12;\xa47_\xd0\x5c]\xc5\xc2\xad\xe86\ -B\x04#\x84\x9d\xa5?\xd5\x83\x1a\xe2\x83\x1d\xa2\xb1\xc1\ -\xed\xa0\xb0\xa4\xb2\xe1\xfd4u\xdd\xbe\xef #\x98\x14\ -\xb4\xd6\xd0S\xd8_:\xfc\xf3\xd4\xf5\x87\xf3\xd3Qn\ -\xdc\x141rj\xb88yE\x03\xb6\xf9\x0ej(\x08\ -\xec\x10\x8d\x8do\x07\x85e\x97\x8d0\xfdND\x9b\xbc\ -\xa0\xad\x12L^\xb8\xf6\xf0\x97\xf0\xe9\xa9\x90\x87\x1b!\ -l$aH\x97'\xaf\xdaWKPC\xa1`\x87h\ -l\x16;\x84\xed\xdd\xc7\xa3\xf6\x1d\x8dC\xc3=\xf2\x82\ -\x1d\xb4GC\xb1pp\x1d#\x0c\xc3\xfe\xb1\x9a\xda\xc8\ -\x086\xebA\x0d\x85\x82\x1d\xa2\xb1)\xec\xa0\x08\x99\xaf\ -\xad\xdd\x812=\x9c\x1c\x09\xd8\x9fv|}#\x8c\xc7\ -\xfe\xab7\xa6o\xda\x82(\xfd\x9f\xa3\x86b\xc1\x0e\xd1\ -\xd8,vP\xe4\xf2?d\xa0\xed\xdb\xceF\x8800\ -\xbb\xe3\xfe\xad\x99~MvvV\xbf*\x82\x1a\x0a\x07\ -;Dcs\xd9\xa1\x9au\xb5\xaff\xd8\xc1\xf4\xc1\x8d\ -\x12aT\xf6\xff\xec\x9f\x19\xf0~\xa1\x07\x0a\x04;D\ -c\x13\xd9\xc1B\x89\xb71\xbd\xa0\xc8\xa9\xe1\xf7\xd9;\ -\xf6s\xe1\xb5\xb7`\xb1\xc3Z\x80\x1d\xa2\xb1\xe9\xec\xb0\ -\x91\xc3\xec`\xff\xc9\xda\xbd\x1b\xec\x17\x03Q\xc3\xda\x81\ -\x1d\xa2\x81\x1dbEV\x0d\xc3\x8fG:\x07\xbb\xf4\xbf\ -\xad-(P\xc3\xda\x81\x1d\xa2\x81\x1d\xa2DV\x0d\xa3\ -s\xf7\xf7\x0e\x1d\xcc\xa8a\xc9?\x1c\x8a\x06;D#\ -k\x07\x9d\xe2\xd02\xa6\x86\xf1'\xe5\xfd\xa5\xc3\xdb\xab\ -?\xe1\x8b\x1a\xd6\x01\xec\x10\x0d\xd9\xe1\xd0p\x8fNk\ -b\xf5Q\xaeL\x1c(\x1d\xcd|U\x0c;\xac\x03\xd8\ -!\x1a\xb2\x83N\xe8\xcaB\xe5\xef\xca\x94\xdd\xd3\x11V\ -\xca\xc3\xf9\xe9\xd9\xf9Gcs\xe3\xdd\xc3\xc7k_\x12\ -\xd3\xff\x165\xac\x0f\xd8!\x0av\xfaj\x0b\x11\xd8\x99\ -\xfe\xfe\xc5\xd2\xff-\xac\x03\xd8!\x22\x9c\xc7\xd1\xe1_\ -\xba\x9e`\x87\x88p*G\x84\x7f\xe6\xfa\x83\x1d\x00\xc0\ -\x07;\x00\x80\x0fv\x00\x00\x1f\xec\x00\x00>\xd8\x01\x00\ -|\xb0\x03\x00\xf8`\x07\x00\xf0\xc1\x0e\x00\xe0\x83\x1d\x00\ -\xc0\x07;\x00\x80\x0fv\x00\x00\x1f\xec\x00\x00>\xd8\x01\ -\x00|\xb0\x03\x00\xf8`\x07\x00\xf0\xc1\x0e\x00\xe0\x83\x1d\ -\x00\xc0\x07;\x00\x80\x0fv\x00\x00\x1f\xec\x00\x00>\xd8\ -\x01\x00|\xb0\x03\x00\xf8`\x07\x00\xf0\xc1\x0e\x00\xe0\x83\ -\x1d\x00\xc0\x07;\x00\x80\x0fv\x00\x00\x1f\xec\x00\x00>\ -\xd8\x01\x00|\xb0\x03\x00\xf8`\x07\x00\xf0\xc1\x0e\x00\xe0\ -\x83\x1d\x00\xc0\x07;\x00\x80\x0fv\x00\x00\x1f\xec\x00\x00\ ->\xd8\x01\x00|\xb0\x03\x00\xf8`\x07\x00\xf0\xc1\x0e\x00\ -\xe0\x83\x1d\x00\xc0\x07;\x00\x80\x0fv\x00\x00\x1f\xec\x00\ -\x00>\xd8\x01\x00|\xb0\x03\x00\xf8`\x07\x00\xf0\xc1\x0e\ -\x00\xe0\x83\x1d\x00\xc0\x07;\x00\x80\x0fv\x00\x00\x1f\xec\ -\x00\x00>\xd8\x01\x00|\xb0\x03\x00\xf8`\x07\x00\xf0\xc1\ -\x0e\x00\xe0\x83\x1d\x00\xc0\x07;\x00\x80\x0fv\x00\x00\x1f\ -\xec\x00\x00>\xd8\x01\x00|\xb0\x03\x00\xf8`\x07\x00\xf0\ -\xc1\x0e\x00\xe0\x83\x1d\x00\xc0\x07;\x00\x80\x0fv\x00\x00\ -\x1f\xec\x00\x00>\xd8\x01\x00|\xb0\x03\x00\xf8`\x07\x00\ -\xf0\xc1\x0e\x00\xe0\x83\x1d\x00\xc0\x07;\x00\x80\x0fv\x00\ -\x00\x1f\xec\x00\x00>\xd8\x01\x00|\xb0\x03\x00\xf8`\x07\ -\x00\xf0\xc1\x0e\x00\xe0\x83\x1d\x00\xc0\x07;\x00\x80\x0fv\ -\x00\x00\x1f\xec\x00\x00>\xd8\x01\x00|\xb0\x03\x00\xf8`\ -\x07\x00\xf0\xc1\x0e\x00\xe0\x83\x1d\x00\xc0\x07;\x00\x80\x0f\ -v\x00\x00\x1f\xec\x00\x00>\xd8\x01\x00|\xb0\x03\x00\xf8\ -`\x07\x00\xf0\xc1\x0e\x00\xe0\x83\x1d\x00\xc0\xe7\x1f\xff\xfe\ -\xef\x1b\x00\x00y\xfe\xfb\xc6\xff\x034\xc3}\xffR)\ -\xdb\xa4\x00\x00\x00\x00IEND\xaeB`\x82\ -" - -qt_resource_name = b"\ -\x00\x08\ -\x08\x01Z\x5c\ -\x00m\ -\x00a\x00i\x00n\x00.\x00q\x00m\x00l\ -\x00\x10\ -\x0d\x0d\xd3\xc7\ -\x00q\ -\x00t\x00_\x00l\x00o\x00g\x00o\x00_\x00r\x00e\x00c\x00t\x00.\x00p\x00n\x00g\ -" - -qt_resource_struct = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01}k\x86\xb3\x9c\ -\x00\x00\x00\x16\x00\x00\x00\x00\x00\x01\x00\x00\x09G\ -\x00\x00\x01}k\x86\xb3\x9c\ -" - -def qInitResources(): - QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data) - -def qCleanupResources(): - QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data) - -qInitResources() diff --git a/examples/quick3d/intro/main.qml b/examples/quick3d/intro/main.qml index 648cfcf5..ab4e6a6d 100644 --- a/examples/quick3d/intro/main.qml +++ b/examples/quick3d/intro/main.qml @@ -42,8 +42,8 @@ Window { position: Qt.vector3d(0, -200, 0) source: "#Cylinder" scale: Qt.vector3d(2, 0.2, 1) - materials: [ DefaultMaterial { - diffuseColor: "red" + materials: [ PrincipledMaterial { + baseColor: "red" } ] } @@ -52,8 +52,8 @@ Window { position: Qt.vector3d(0, 150, 0) source: "#Sphere" - materials: [ DefaultMaterial { - diffuseColor: "blue" + materials: [ PrincipledMaterial { + baseColor: "blue" } ] diff --git a/examples/quickcontrols/contactslist/Contact/ContactDelegate.ui.qml b/examples/quickcontrols/contactslist/Contact/ContactDelegate.ui.qml index affcccc3..e1e6127b 100644 --- a/examples/quickcontrols/contactslist/Contact/ContactDelegate.ui.qml +++ b/examples/quickcontrols/contactslist/Contact/ContactDelegate.ui.qml @@ -7,14 +7,18 @@ import QtQuick.Controls ItemDelegate { id: delegate - checkable: true + required property string fullName + required property string address + required property string city + required property string number + contentItem: ColumnLayout { spacing: 10 Label { - text: fullName + text: delegate.fullName font.bold: true elide: Text.ElideRight Layout.fillWidth: true @@ -34,7 +38,7 @@ ItemDelegate { } Label { - text: address + text: delegate.address font.bold: true elide: Text.ElideRight Layout.fillWidth: true @@ -46,7 +50,7 @@ ItemDelegate { } Label { - text: city + text: delegate.city font.bold: true elide: Text.ElideRight Layout.fillWidth: true @@ -58,7 +62,7 @@ ItemDelegate { } Label { - text: number + text: delegate.number font.bold: true elide: Text.ElideRight Layout.fillWidth: true @@ -74,6 +78,7 @@ ItemDelegate { PropertyChanges { // TODO: When Qt Design Studio supports generalized grouped properties, change to: // grid.visible: true + // qmllint disable Quick.property-changes-parsed target: grid visible: true } diff --git a/examples/quickcontrols/contactslist/Contact/ContactDialog.qml b/examples/quickcontrols/contactslist/Contact/ContactDialog.qml index d906f00e..3f287447 100644 --- a/examples/quickcontrols/contactslist/Contact/ContactDialog.qml +++ b/examples/quickcontrols/contactslist/Contact/ContactDialog.qml @@ -41,5 +41,9 @@ Dialog { id: form } - onAccepted: finished(form.fullName.text, form.address.text, form.city.text, form.number.text) + onAccepted: { + if (form.fullName.text && form.address.text && form.city.text && form.number.text) { + finished(form.fullName.text, form.address.text, form.city.text, form.number.text); + } + } } diff --git a/examples/quickcontrols/contactslist/Contact/ContactList.qml b/examples/quickcontrols/contactslist/Contact/ContactList.qml index 0b7af32b..121b38f3 100644 --- a/examples/quickcontrols/contactslist/Contact/ContactList.qml +++ b/examples/quickcontrols/contactslist/Contact/ContactList.qml @@ -17,10 +17,10 @@ ApplicationWindow { ContactDialog { id: contactDialog onFinished: function(fullName, address, city, number) { - if (currentContact == -1) + if (window.currentContact === -1) contactView.model.append(fullName, address, city, number) else - contactView.model.set(currentContact, fullName, address, city, number) + contactView.model.set(window.currentContact, fullName, address, city, number) } } @@ -35,23 +35,23 @@ ApplicationWindow { font.bold: true width: parent.width horizontalAlignment: Qt.AlignHCenter - text: currentContact >= 0 ? contactView.model.get(currentContact).fullName : "" + text: window.currentContact >= 0 ? contactView.model.get(window.currentContact).fullName : "" } MenuItem { text: qsTr("Edit...") - onTriggered: contactDialog.editContact(contactView.model.get(currentContact)) + onTriggered: contactDialog.editContact(contactView.model.get(window.currentContact)) } MenuItem { text: qsTr("Remove") - onTriggered: contactView.model.remove(currentContact) + onTriggered: contactView.model.remove(window.currentContact) } } ContactView { id: contactView anchors.fill: parent - onPressAndHold: { - currentContact = index + onPressAndHold: function(index) { + window.currentContact = index contactMenu.open() } } @@ -63,7 +63,7 @@ ApplicationWindow { anchors.right: parent.right anchors.bottom: parent.bottom onClicked: { - currentContact = -1 + window.currentContact = -1 contactDialog.createContact() } } diff --git a/examples/quickcontrols/contactslist/Contact/ContactView.ui.qml b/examples/quickcontrols/contactslist/Contact/ContactView.ui.qml index 3b82b681..707888e7 100644 --- a/examples/quickcontrols/contactslist/Contact/ContactView.ui.qml +++ b/examples/quickcontrols/contactslist/Contact/ContactView.ui.qml @@ -1,6 +1,8 @@ // Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Controls import Backend @@ -25,6 +27,9 @@ ListView { delegate: ContactDelegate { id: delegate width: listView.width + + required property int index + onPressAndHold: listView.pressAndHold(index) } diff --git a/examples/quickcontrols/contactslist/Contact/SectionDelegate.ui.qml b/examples/quickcontrols/contactslist/Contact/SectionDelegate.ui.qml index 3a62409a..1ed587ab 100644 --- a/examples/quickcontrols/contactslist/Contact/SectionDelegate.ui.qml +++ b/examples/quickcontrols/contactslist/Contact/SectionDelegate.ui.qml @@ -7,9 +7,11 @@ import QtQuick.Controls ToolBar { id: background + required property string section + Label { id: label - text: section + text: background.section anchors.fill: parent horizontalAlignment: Qt.AlignHCenter verticalAlignment: Qt.AlignVCenter diff --git a/examples/quickcontrols/contactslist/contactmodel.py b/examples/quickcontrols/contactslist/contactmodel.py index 9f17786c..848ce54c 100644 --- a/examples/quickcontrols/contactslist/contactmodel.py +++ b/examples/quickcontrols/contactslist/contactmodel.py @@ -80,7 +80,7 @@ class ContactModel(QAbstractListModel): default[ContactModel.ContactRole.NumberRole] = QByteArray(b"number") return default - @Slot(int) + @Slot(int, result="QVariantMap") def get(self, row: int): contact = self.m_contacts[row] return {"fullName": contact.fullName, "address": contact.address, @@ -101,11 +101,11 @@ class ContactModel(QAbstractListModel): return self.m_contacts[row] = self.Contact(full_name, address, city, number) - self.dataChanged(self.index(row, 0), self.index(row, 0), - [ContactModel.ContactRole.FullNameRole, - ContactModel.ContactRole.AddressRole, - ContactModel.ContactRole.CityRole, - ContactModel.ContactRole.NumberRole]) + roles = [ContactModel.ContactRole.FullNameRole, + ContactModel.ContactRole.AddressRole, + ContactModel.ContactRole.CityRole, + ContactModel.ContactRole.NumberRole] + self.dataChanged.emit(self.index(row, 0), self.index(row, 0), roles) @Slot(int) def remove(self, row): diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/Main.qml b/examples/quickcontrols/filesystemexplorer/FileSystemModule/Main.qml index 7f7798ed..36f2ac3b 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/Main.qml +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/Main.qml @@ -1,6 +1,5 @@ // Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - import QtQuick import QtQuick.Controls.Basic import QtQuick.Layouts @@ -22,7 +21,7 @@ ApplicationWindow { visible: true color: Colors.background flags: Qt.Window | Qt.FramelessWindowHint - title: qsTr("File System Explorer Example") + title: qsTr("File System Explorer") function getInfoText() : string { let out = root.currentFilePath diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/About.qml b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/About.qml index 178bf03e..0d308a2a 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/About.qml +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/About.qml @@ -16,7 +16,7 @@ ApplicationWindow { id: menuBar dragWindow: root - implicitHeight: 27 + implicitHeight: 30 infoText: "About Qt" } diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Editor.qml b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Editor.qml index 80f7c04c..2f995c88 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Editor.qml +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Editor.qml @@ -36,6 +36,7 @@ Rectangle { Layout.preferredWidth: fontMetrics.averageCharacterWidth * (Math.floor(Math.log10(textArea.lineCount)) + 1) + 10 Layout.fillHeight: true + Layout.fillWidth: false interactive: false contentY: editorFlickable.contentY diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/ResizeButton.qml b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/ResizeButton.qml index 0df65bf8..5d3b68b3 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/ResizeButton.qml +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/ResizeButton.qml @@ -1,6 +1,7 @@ // Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +import QtQml import QtQuick.Controls import FileSystemModule diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Sidebar.qml b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Sidebar.qml index aac53039..f739e0f9 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Sidebar.qml +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Sidebar.qml @@ -50,6 +50,8 @@ Rectangle { id: tabBarComponent Layout.fillWidth: true + Layout.fillHeight: false + // ButtonGroup ensures that only one button can be checked at a time. ButtonGroup { buttons: tabBarComponent.contentChildren diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qmldir b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qmldir index b1f68460..f94e68a8 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qmldir +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qmldir @@ -1,8 +1,9 @@ module FileSystemModule + Main 1.0 Main.qml About 1.0 qml/About.qml -Editor 1.0 qml/Editor.qml MyMenu 1.0 qml/MyMenu.qml +Editor 1.0 qml/Editor.qml Sidebar 1.0 qml/Sidebar.qml MyMenuBar 1.0 qml/MyMenuBar.qml singleton Colors 1.0 qml/Colors.qml diff --git a/examples/quickcontrols/gallery/gallery.py b/examples/quickcontrols/gallery/gallery.py index d454cf53..a7660087 100644 --- a/examples/quickcontrols/gallery/gallery.py +++ b/examples/quickcontrols/gallery/gallery.py @@ -35,9 +35,11 @@ if __name__ == "__main__": engine = QQmlApplicationEngine() - built_in_styles = ["Basic", "Fusion", "Imagine", "Material", "Universal"] + built_in_styles = ["Basic", "Fusion", "Imagine", "Material", "Universal", "FluentWinUI3"] + if platform.system() == "Darwin": built_in_styles.append("macOS") + built_in_styles.append("iOS") elif platform.system() == "Windows": built_in_styles.append("Windows") engine.setInitialProperties({"builtInStyles": built_in_styles}) diff --git a/examples/quickcontrols/gallery/gallery.pyproject b/examples/quickcontrols/gallery/gallery.pyproject index 5b5fe35d..f285c24b 100644 --- a/examples/quickcontrols/gallery/gallery.pyproject +++ b/examples/quickcontrols/gallery/gallery.pyproject @@ -6,31 +6,39 @@ "qtquickcontrols2.conf", "ToolBar.qml", "+Material/ToolBar.qml", + "pages/BusyIndicatorPage.qml", + "pages/ButtonPage.qml", + "pages/CheckBoxPage.qml", "pages/ComboBoxPage.qml", + "pages/DelayButtonPage.qml", + "pages/DelegatePage.qml", + "pages/DialogPage.qml", + "pages/DialPage.qml", "pages/FramePage.qml", + "pages/GalleryConfig.qml", + "pages/GroupBoxPage.qml", + "pages/MenuBarPage.qml", + "pages/MonthGridPage.qml", + "pages/PageIndicatorPage.qml", + "pages/ProgressBarPage.qml", + "pages/RadioButtonPage.qml", + "pages/RangeSliderPage.qml", + "pages/ScrollablePage.qml", + "pages/ScrollBarPage.qml", + "pages/ScrollIndicatorPage.qml", + "pages/SearchFieldPage.qml", "pages/SliderPage.qml", - "pages/TumblerPage.qml", "pages/SpinBoxPage.qml", - "pages/ProgressBarPage.qml", - "pages/DelegatePage.qml", + "pages/SplitViewPage.qml", "pages/StackViewPage.qml", - "pages/DialPage.qml", - "pages/PageIndicatorPage.qml", "pages/SwipeViewPage.qml", + "pages/SwitchPage.qml", "pages/TabBarPage.qml", + "pages/TableViewPage.qml", + "pages/TextAreaPage.qml", "pages/TextFieldPage.qml", - "pages/GroupBoxPage.qml", - "pages/RadioButtonPage.qml", - "pages/ButtonPage.qml", - "pages/ScrollIndicatorPage.qml", - "pages/ScrollablePage.qml", - "pages/DialogPage.qml", + "pages/ToolBarPage.qml", "pages/ToolTipPage.qml", - "pages/CheckBoxPage.qml", - "pages/TextAreaPage.qml", - "pages/RangeSliderPage.qml", - "pages/DelayButtonPage.qml", - "pages/SwitchPage.qml", - "pages/ScrollBarPage.qml", - "pages/BusyIndicatorPage.qml"] + "pages/TreeViewPage.qml", + "pages/TumblerPage.qml"] } diff --git a/examples/quickcontrols/gallery/gallery.qml b/examples/quickcontrols/gallery/gallery.qml index 65851f8c..4b699038 100644 --- a/examples/quickcontrols/gallery/gallery.qml +++ b/examples/quickcontrols/gallery/gallery.qml @@ -15,10 +15,10 @@ ApplicationWindow { width: 360 height: 520 visible: true - title: "Qt Quick Controls" + title: qsTr("Qt Quick Controls") //! [orientation] - readonly property bool portraitMode: window.width < window.height + readonly property bool portraitMode: !orientationCheckBox.checked || window.width < window.height //! [orientation] function help() { @@ -46,7 +46,7 @@ ApplicationWindow { } Shortcut { - sequence: StandardKey.HelpContents + sequences: [StandardKey.HelpContents] onActivated: window.help() } @@ -63,11 +63,6 @@ ApplicationWindow { } } - Shortcut { - sequence: "Menu" - onActivated: optionsMenuAction.trigger() - } - Action { id: optionsMenuAction icon.name: "menu" @@ -87,7 +82,7 @@ ApplicationWindow { Label { id: titleLabel - text: listView.currentItem ? (listView.currentItem as ItemDelegate).text : "Gallery" + text: listView.currentItem ? (listView.currentItem as ItemDelegate).text : qsTr("Gallery") font.pixelSize: 20 elide: Label.ElideRight horizontalAlignment: Qt.AlignHCenter @@ -104,15 +99,15 @@ ApplicationWindow { transformOrigin: Menu.TopRight Action { - text: "Settings" + text: qsTr("Settings") onTriggered: settingsDialog.open() } Action { - text: "Help" + text: qsTr("Help") onTriggered: window.help() } Action { - text: "About" + text: qsTr("About") onTriggered: aboutDialog.open() } } @@ -138,32 +133,39 @@ ApplicationWindow { anchors.fill: parent model: ListModel { - ListElement { title: "BusyIndicator"; source: "qrc:/pages/BusyIndicatorPage.qml" } - ListElement { title: "Button"; source: "qrc:/pages/ButtonPage.qml" } - ListElement { title: "CheckBox"; source: "qrc:/pages/CheckBoxPage.qml" } - ListElement { title: "ComboBox"; source: "qrc:/pages/ComboBoxPage.qml" } - ListElement { title: "DelayButton"; source: "qrc:/pages/DelayButtonPage.qml" } - ListElement { title: "Dial"; source: "qrc:/pages/DialPage.qml" } - ListElement { title: "Dialog"; source: "qrc:/pages/DialogPage.qml" } - ListElement { title: "Delegates"; source: "qrc:/pages/DelegatePage.qml" } - ListElement { title: "Frame"; source: "qrc:/pages/FramePage.qml" } - ListElement { title: "GroupBox"; source: "qrc:/pages/GroupBoxPage.qml" } - ListElement { title: "PageIndicator"; source: "qrc:/pages/PageIndicatorPage.qml" } - ListElement { title: "ProgressBar"; source: "qrc:/pages/ProgressBarPage.qml" } - ListElement { title: "RadioButton"; source: "qrc:/pages/RadioButtonPage.qml" } - ListElement { title: "RangeSlider"; source: "qrc:/pages/RangeSliderPage.qml" } - ListElement { title: "ScrollBar"; source: "qrc:/pages/ScrollBarPage.qml" } - ListElement { title: "ScrollIndicator"; source: "qrc:/pages/ScrollIndicatorPage.qml" } - ListElement { title: "Slider"; source: "qrc:/pages/SliderPage.qml" } - ListElement { title: "SpinBox"; source: "qrc:/pages/SpinBoxPage.qml" } - ListElement { title: "StackView"; source: "qrc:/pages/StackViewPage.qml" } - ListElement { title: "SwipeView"; source: "qrc:/pages/SwipeViewPage.qml" } - ListElement { title: "Switch"; source: "qrc:/pages/SwitchPage.qml" } - ListElement { title: "TabBar"; source: "qrc:/pages/TabBarPage.qml" } - ListElement { title: "TextArea"; source: "qrc:/pages/TextAreaPage.qml" } - ListElement { title: "TextField"; source: "qrc:/pages/TextFieldPage.qml" } - ListElement { title: "ToolTip"; source: "qrc:/pages/ToolTipPage.qml" } - ListElement { title: "Tumbler"; source: "qrc:/pages/TumblerPage.qml" } + ListElement { title: qsTr("BusyIndicator"); source: "qrc:/pages/BusyIndicatorPage.qml" } + ListElement { title: qsTr("Button"); source: "qrc:/pages/ButtonPage.qml" } + ListElement { title: qsTr("CheckBox"); source: "qrc:/pages/CheckBoxPage.qml" } + ListElement { title: qsTr("ComboBox"); source: "qrc:/pages/ComboBoxPage.qml" } + ListElement { title: qsTr("DelayButton"); source: "qrc:/pages/DelayButtonPage.qml" } + ListElement { title: qsTr("Dial"); source: "qrc:/pages/DialPage.qml" } + ListElement { title: qsTr("Dialog"); source: "qrc:/pages/DialogPage.qml" } + ListElement { title: qsTr("Delegates"); source: "qrc:/pages/DelegatePage.qml" } + ListElement { title: qsTr("Frame"); source: "qrc:/pages/FramePage.qml" } + ListElement { title: qsTr("GroupBox"); source: "qrc:/pages/GroupBoxPage.qml" } + ListElement { title: qsTr("MenuBar"); source: "qrc:/pages/MenuBarPage.qml" } + ListElement { title: qsTr("MonthGrid"); source: "qrc:/pages/MonthGridPage.qml" } + ListElement { title: qsTr("PageIndicator"); source: "qrc:/pages/PageIndicatorPage.qml" } + ListElement { title: qsTr("ProgressBar"); source: "qrc:/pages/ProgressBarPage.qml" } + ListElement { title: qsTr("RadioButton"); source: "qrc:/pages/RadioButtonPage.qml" } + ListElement { title: qsTr("RangeSlider"); source: "qrc:/pages/RangeSliderPage.qml" } + ListElement { title: qsTr("ScrollBar"); source: "qrc:/pages/ScrollBarPage.qml" } + ListElement { title: qsTr("ScrollIndicator"); source: "qrc:/pages/ScrollIndicatorPage.qml" } + ListElement { title: qsTr("SearchField"); source: "qrc:/pages/SearchFieldPage.qml" } + ListElement { title: qsTr("Slider"); source: "qrc:/pages/SliderPage.qml" } + ListElement { title: qsTr("SpinBox"); source: "qrc:/pages/SpinBoxPage.qml" } + ListElement { title: qsTr("SplitView"); source: "qrc:/pages/SplitViewPage.qml" } + ListElement { title: qsTr("StackView"); source: "qrc:/pages/StackViewPage.qml" } + ListElement { title: qsTr("SwipeView"); source: "qrc:/pages/SwipeViewPage.qml" } + ListElement { title: qsTr("Switch"); source: "qrc:/pages/SwitchPage.qml" } + ListElement { title: qsTr("TabBar"); source: "qrc:/pages/TabBarPage.qml" } + ListElement { title: qsTr("TableView"); source: "qrc:/pages/TableViewPage.qml" } + ListElement { title: qsTr("TextArea"); source: "qrc:/pages/TextAreaPage.qml" } + ListElement { title: qsTr("TextField"); source: "qrc:/pages/TextFieldPage.qml" } + ListElement { title: qsTr("ToolBar"); source: "qrc:/pages/ToolBarPage.qml" } + ListElement { title: qsTr("ToolTip"); source: "qrc:/pages/ToolTipPage.qml" } + ListElement { title: qsTr("TreeView"); source: "qrc:/pages/TreeViewPage.qml" } + ListElement { title: qsTr("Tumbler"); source: "qrc:/pages/TumblerPage.qml" } } delegate: ItemDelegate { @@ -178,6 +180,9 @@ ApplicationWindow { required property string source onClicked: { + if (stackView.depth > 1) + return + listView.currentIndex = index stackView.push(source) if (window.portraitMode) @@ -209,7 +214,7 @@ ApplicationWindow { } Label { - text: "Qt Quick Controls provides a set of controls that can be used to build complete interfaces in Qt Quick." + text: qsTr("Qt Quick Controls provides a set of controls that can be used to build complete interfaces in Qt Quick.") anchors.margins: 20 anchors.top: logo.bottom anchors.left: parent.left @@ -234,14 +239,14 @@ ApplicationWindow { id: settingsDialog x: Math.round((window.width - width) / 2) y: Math.round(window.height / 6) - width: Math.round(Math.min(window.width, window.height) / 3 * 2) modal: true focus: true - title: "Settings" + title: qsTr("Settings") standardButtons: Dialog.Ok | Dialog.Cancel onAccepted: { settings.style = styleBox.displayText + GalleryConfig.disabled = disableControlsCheckBox.checked settingsDialog.close() } onRejected: { @@ -257,7 +262,7 @@ ApplicationWindow { spacing: 10 Label { - text: "Style:" + text: qsTr("Style:") } ComboBox { @@ -273,8 +278,61 @@ ApplicationWindow { } } + RowLayout { + id: colorSchemes + // Some Qt Quick styles prioritize the respective design system guidelines + // over the system palette. + enabled: ["FluentWinUI3", "Fusion", "iOS", "Basic"].includes(styleBox.currentText) + CheckBox { + id: autoColorScheme + checked: true + text: qsTr("Auto") + } + CheckBox { + id: darkColorScheme + text: qsTr("Dark Mode") + } + CheckBox { + id: lightColorScheme + text: qsTr("Light Mode") + } + ButtonGroup { + exclusive: true + buttons: colorSchemes.children + onCheckedButtonChanged: { + let scheme; + switch (checkedButton) { + case autoColorScheme: + scheme = Qt.Unknown + break; + case darkColorScheme: + scheme = Qt.Dark + break; + case lightColorScheme: + scheme = Qt.Light + break; + } + Qt.styleHints.colorScheme = scheme + } + } + } + + CheckBox { + id: orientationCheckBox + text: qsTr("Enable Landscape") + checked: false + Layout.fillWidth: true + } + + CheckBox { + id: disableControlsCheckBox + checked: GalleryConfig.disabled + text: qsTr("Disable Controls") + Layout.fillWidth: true + } + Label { - text: "Restart required" + text: qsTr("Restart required") color: "#e41e25" opacity: styleBox.currentIndex !== styleBox.styleIndex ? 1.0 : 0.0 horizontalAlignment: Label.AlignHCenter @@ -289,7 +347,7 @@ ApplicationWindow { id: aboutDialog modal: true focus: true - title: "About" + title: qsTr("About") x: (window.width - width) / 2 y: window.height / 6 width: Math.min(window.width, window.height) / 3 * 2 @@ -301,15 +359,15 @@ ApplicationWindow { Label { width: aboutDialog.availableWidth - text: "The Qt Quick Controls module delivers the next generation user interface controls based on Qt Quick." + text: qsTr("The Qt Quick Controls module delivers the next generation user interface controls based on Qt Quick.") wrapMode: Label.Wrap font.pixelSize: 12 } Label { width: aboutDialog.availableWidth - text: "In comparison to Qt Quick Controls 1, Qt Quick Controls " - + "are an order of magnitude simpler, lighter, and faster." + text: qsTr("In comparison to Qt Quick Controls 1, Qt Quick Controls " + + "are an order of magnitude simpler, lighter, and faster.") wrapMode: Label.Wrap font.pixelSize: 12 } diff --git a/examples/quickcontrols/gallery/gallery.qrc b/examples/quickcontrols/gallery/gallery.qrc index 33019794..41c8d25a 100644 --- a/examples/quickcontrols/gallery/gallery.qrc +++ b/examples/quickcontrols/gallery/gallery.qrc @@ -37,7 +37,10 @@ pages/DialPage.qml pages/DialogPage.qml pages/FramePage.qml + pages/GalleryConfig.qml pages/GroupBoxPage.qml + pages/MenuBarPage.qml + pages/MonthGridPage.qml pages/PageIndicatorPage.qml pages/ProgressBarPage.qml pages/RadioButtonPage.qml @@ -45,15 +48,20 @@ pages/ScrollBarPage.qml pages/ScrollIndicatorPage.qml pages/ScrollablePage.qml + pages/SearchFieldPage.qml pages/SliderPage.qml pages/SpinBoxPage.qml + pages/SplitViewPage.qml pages/StackViewPage.qml pages/SwipeViewPage.qml pages/SwitchPage.qml pages/TabBarPage.qml + pages/TableViewPage.qml pages/TextAreaPage.qml pages/TextFieldPage.qml + pages/ToolBarPage.qml pages/ToolTipPage.qml + pages/TreeViewPage.qml pages/TumblerPage.qml qmldir qtquickcontrols2.conf diff --git a/examples/quickcontrols/gallery/pages/BusyIndicatorPage.qml b/examples/quickcontrols/gallery/pages/BusyIndicatorPage.qml index 5f391abf..7cdc4b49 100644 --- a/examples/quickcontrols/gallery/pages/BusyIndicatorPage.qml +++ b/examples/quickcontrols/gallery/pages/BusyIndicatorPage.qml @@ -17,8 +17,8 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "BusyIndicator is used to indicate activity while content is being loaded," - + " or when the UI is blocked waiting for a resource to become available." + text: qsTr("BusyIndicator is used to indicate activity while content is being loaded," + + " or when the UI is blocked waiting for a resource to become available.") } BusyIndicator { diff --git a/examples/quickcontrols/gallery/pages/ButtonPage.qml b/examples/quickcontrols/gallery/pages/ButtonPage.qml index 06051767..a5aab7d3 100644 --- a/examples/quickcontrols/gallery/pages/ButtonPage.qml +++ b/examples/quickcontrols/gallery/pages/ButtonPage.qml @@ -12,12 +12,30 @@ ScrollablePage { spacing: 40 width: parent.width + Row { + CheckBox { + id: checkedCheckBox + text: qsTr("Checked") + } + + CheckBox { + id: flatCheckBox + text: qsTr("Flat") + } + + CheckBox { + id: pressedCheckBox + enabled: !GalleryConfig.disabled + text: qsTr("Pressed") + } + } + Label { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "Button presents a push-button that can be pushed or clicked by the user. " - + "Buttons are normally used to perform an action, or to answer a question." + text: qsTr("Button presents a push-button that can be pushed or clicked by the user. " + + "Buttons are normally used to perform an action, or to answer a question.") } ColumnLayout { @@ -25,18 +43,28 @@ ScrollablePage { anchors.horizontalCenter: parent.horizontalCenter Button { - text: "First" + enabled: !GalleryConfig.disabled + text: qsTr("Button") + checked: checkedCheckBox.checked + flat: flatCheckBox.checked + down: pressedCheckBox.checked ? true : undefined Layout.fillWidth: true } Button { - id: button - text: "Second" + enabled: !GalleryConfig.disabled + text: qsTr("Highlighted") + checked: checkedCheckBox.checked + flat: flatCheckBox.checked + down: pressedCheckBox.checked ? true : undefined highlighted: true Layout.fillWidth: true } - Button { - text: "Third" - enabled: false + RoundButton { + enabled: !GalleryConfig.disabled + text: qsTr("RoundButton") + checked: checkedCheckBox.checked + flat: flatCheckBox.checked + down: pressedCheckBox.checked ? true : undefined Layout.fillWidth: true } } diff --git a/examples/quickcontrols/gallery/pages/CheckBoxPage.qml b/examples/quickcontrols/gallery/pages/CheckBoxPage.qml index 003e44c8..cbc644e9 100644 --- a/examples/quickcontrols/gallery/pages/CheckBoxPage.qml +++ b/examples/quickcontrols/gallery/pages/CheckBoxPage.qml @@ -15,8 +15,8 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "CheckBox presents an option button that can be toggled on or off. " - + "Check boxes are typically used to select one or more options from a set of options." + text: qsTr("CheckBox presents an option button that can be toggled on or off. " + + "Check boxes are typically used to select one or more options from a set of options.") } Column { @@ -24,16 +24,13 @@ ScrollablePage { anchors.horizontalCenter: parent.horizontalCenter CheckBox { - text: "First" + enabled: !GalleryConfig.disabled + text: qsTr("First") checked: true } CheckBox { - text: "Second" - } - CheckBox { - text: "Third" - checked: true - enabled: false + enabled: !GalleryConfig.disabled + text: qsTr("Second") } } } diff --git a/examples/quickcontrols/gallery/pages/ComboBoxPage.qml b/examples/quickcontrols/gallery/pages/ComboBoxPage.qml index 2dc10cee..ff764d65 100644 --- a/examples/quickcontrols/gallery/pages/ComboBoxPage.qml +++ b/examples/quickcontrols/gallery/pages/ComboBoxPage.qml @@ -15,12 +15,13 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "ComboBox is a combined button and popup list. It presents " - + "a list of options to the user that occupies minimal screen space." + text: qsTr("ComboBox is a combined button and popup list. It presents " + + "a list of options to the user that occupies minimal screen space.") } ComboBox { - model: ["First", "Second", "Third"] + enabled: !GalleryConfig.disabled + model: [qsTr("First"), qsTr("Second"), qsTr("Third")] anchors.horizontalCenter: parent.horizontalCenter } @@ -28,18 +29,19 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "ComboBox can be made \l editable. An editable combo box auto-" - + "completes its text based on what is available in the model." + text: qsTr("ComboBox can be made editable. An editable combo box auto-" + + "completes its text based on what is available in the model.") } ComboBox { id: comboBox + enabled: !GalleryConfig.disabled editable: true model: ListModel { - ListElement { text: "Banana" } - ListElement { text: "Apple" } - ListElement { text: "Coconut" } + ListElement { text: qsTr("Banana") } + ListElement { text: qsTr("Apple") } + ListElement { text: qsTr("Coconut") } } onAccepted: { if (find(editText) === -1) diff --git a/examples/quickcontrols/gallery/pages/DelayButtonPage.qml b/examples/quickcontrols/gallery/pages/DelayButtonPage.qml index 4c0e8725..fc9b922a 100644 --- a/examples/quickcontrols/gallery/pages/DelayButtonPage.qml +++ b/examples/quickcontrols/gallery/pages/DelayButtonPage.qml @@ -15,12 +15,13 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "DelayButton is a checkable button that incorporates a delay before the " - + "button is activated. This delay prevents accidental presses." + text: qsTr("DelayButton is a checkable button that incorporates a delay before the " + + "button is activated. This delay prevents accidental presses.") } DelayButton { - text: "DelayButton" + enabled: !GalleryConfig.disabled + text: qsTr("DelayButton") anchors.horizontalCenter: parent.horizontalCenter } } diff --git a/examples/quickcontrols/gallery/pages/DelegatePage.qml b/examples/quickcontrols/gallery/pages/DelegatePage.qml index 26d346a9..2722f381 100644 --- a/examples/quickcontrols/gallery/pages/DelegatePage.qml +++ b/examples/quickcontrols/gallery/pages/DelegatePage.qml @@ -1,6 +1,8 @@ // Copyright (C) 2017 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Layouts import QtQuick.Controls @@ -15,7 +17,11 @@ Pane { Layout.fillWidth: true wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "Delegate controls are used as delegates in views such as ListView." + text: qsTr("Delegate controls are used as delegates in views such as ListView.") + } + + ButtonGroup { + id: radioButtonGroup } ListView { @@ -37,165 +43,142 @@ Pane { Layout.fillWidth: true Layout.fillHeight: true - readonly property var delegateComponentMap: { - "ItemDelegate": itemDelegateComponent, - "SwipeDelegate": swipeDelegateComponent, - "CheckDelegate": checkDelegateComponent, - "RadioDelegate": radioDelegateComponent, - "SwitchDelegate": switchDelegateComponent + model: ListModel { + ListElement { type: "ItemDelegate"; value: qsTr("ItemDelegate1") } + ListElement { type: "ItemDelegate"; value: qsTr("ItemDelegate2") } + ListElement { type: "ItemDelegate"; value: qsTr("ItemDelegate3") } + ListElement { type: "SwipeDelegate"; value: qsTr("SwipeDelegate1") } + ListElement { type: "SwipeDelegate"; value: qsTr("SwipeDelegate2") } + ListElement { type: "SwipeDelegate"; value: qsTr("SwipeDelegate3") } + ListElement { type: "CheckDelegate"; value: qsTr("CheckDelegate1") } + ListElement { type: "CheckDelegate"; value: qsTr("CheckDelegate2") } + ListElement { type: "CheckDelegate"; value: qsTr("CheckDelegate3") } + ListElement { type: "RadioDelegate"; value: qsTr("RadioDelegate1") } + ListElement { type: "RadioDelegate"; value: qsTr("RadioDelegate2") } + ListElement { type: "RadioDelegate"; value: qsTr("RadioDelegate3") } + ListElement { type: "SwitchDelegate"; value: qsTr("SwitchDelegate1") } + ListElement { type: "SwitchDelegate"; value: qsTr("SwitchDelegate2") } + ListElement { type: "SwitchDelegate"; value: qsTr("SwitchDelegate3") } } - Component { - id: itemDelegateComponent + delegate: Loader { + id: delegateLoader + width: ListView.view.width + sourceComponent: delegateComponentMap[type] - ItemDelegate { - // qmllint disable unqualified - text: value - // qmllint enable unqualified - width: parent.width + required property string value + required property string type + required property var model + required property int index + + property ListView view: listView + + readonly property var delegateComponentMap: { + "ItemDelegate": itemDelegateComponent, + "SwipeDelegate": swipeDelegateComponent, + "CheckDelegate": checkDelegateComponent, + "RadioDelegate": radioDelegateComponent, + "SwitchDelegate": switchDelegateComponent } - } - Component { - id: swipeDelegateComponent - - SwipeDelegate { - id: swipeDelegate - // qmllint disable unqualified - text: value - // qmllint enable unqualified - width: parent.width - - Component { - id: removeComponent - - Rectangle { - color: SwipeDelegate.pressed ? "#333" : "#444" - width: parent.width - height: parent.height - clip: true - - SwipeDelegate.onClicked: { - // qmllint disable unqualified - view.model.remove(ourIndex) - // qmllint enable unqualified - } + Component { + id: itemDelegateComponent + + ItemDelegate { + enabled: !GalleryConfig.disabled + text: delegateLoader.value + width: delegateLoader.width + } + } - Label { - // qmllint disable unqualified - font.pixelSize: swipeDelegate.font.pixelSize - // qmllint enable unqualified - text: "Remove" - color: "white" - anchors.centerIn: parent + Component { + id: swipeDelegateComponent + + SwipeDelegate { + id: swipeDelegate + enabled: !GalleryConfig.disabled + text: delegateLoader.value + width: delegateLoader.width + + Component { + id: removeComponent + + Rectangle { + color: SwipeDelegate.pressed ? "#333" : "#444" + width: parent.width + height: parent.height + clip: true + + SwipeDelegate.onClicked: { + if (delegateLoader.view !== undefined) + delegateLoader.view.model.remove(delegateLoader.index) + } + + Label { + font.pixelSize: swipeDelegate.font.pixelSize + text: qsTr("Remove") + color: "white" + anchors.centerIn: parent + } } } - } - SequentialAnimation { - id: removeAnimation + SequentialAnimation { + id: removeAnimation - PropertyAction { - // qmllint disable unqualified - target: delegateItem - // qmllint enable unqualified - property: "ListView.delayRemove" - value: true - } - NumberAnimation { - // qmllint disable unqualified - target: delegateItem.item - // qmllint enable unqualified - property: "height" - to: 0 - easing.type: Easing.InOutQuad - } - PropertyAction { - // qmllint disable unqualified - target: delegateItem - // qmllint enable unqualified - property: "ListView.delayRemove" - value: false + PropertyAction { + target: delegateLoader + property: "ListView.delayRemove" + value: true + } + NumberAnimation { + target: swipeDelegate + property: "height" + to: 0 + easing.type: Easing.InOutQuad + } + PropertyAction { + target: delegateLoader + property: "ListView.delayRemove" + value: false + } } - } - swipe.left: removeComponent - swipe.right: removeComponent - ListView.onRemove: removeAnimation.start() + swipe.left: removeComponent + swipe.right: removeComponent + ListView.onRemove: removeAnimation.start() + } } - } - Component { - id: checkDelegateComponent + Component { + id: checkDelegateComponent - CheckDelegate { - // qmllint disable unqualified - text: value - // qmllint enable unqualified + CheckDelegate { + enabled: !GalleryConfig.disabled + text: delegateLoader.value + } } - } - ButtonGroup { - id: radioButtonGroup - } + Component { + id: radioDelegateComponent - Component { - id: radioDelegateComponent + RadioDelegate { + enabled: !GalleryConfig.disabled + text: delegateLoader.value - RadioDelegate { - // qmllint disable unqualified - text: value - ButtonGroup.group: radioButtonGroup - // qmllint enable unqualified + ButtonGroup.group: radioButtonGroup + } } - } - Component { - id: switchDelegateComponent + Component { + id: switchDelegateComponent - SwitchDelegate { - // qmllint disable unqualified - text: value - // qmllint enable unqualified + SwitchDelegate { + enabled: !GalleryConfig.disabled + text: delegateLoader.value + } } } - - model: ListModel { - ListElement { type: "ItemDelegate"; value: "ItemDelegate1" } - ListElement { type: "ItemDelegate"; value: "ItemDelegate2" } - ListElement { type: "ItemDelegate"; value: "ItemDelegate3" } - ListElement { type: "SwipeDelegate"; value: "SwipeDelegate1" } - ListElement { type: "SwipeDelegate"; value: "SwipeDelegate2" } - ListElement { type: "SwipeDelegate"; value: "SwipeDelegate3" } - ListElement { type: "CheckDelegate"; value: "CheckDelegate1" } - ListElement { type: "CheckDelegate"; value: "CheckDelegate2" } - ListElement { type: "CheckDelegate"; value: "CheckDelegate3" } - ListElement { type: "RadioDelegate"; value: "RadioDelegate1" } - ListElement { type: "RadioDelegate"; value: "RadioDelegate2" } - ListElement { type: "RadioDelegate"; value: "RadioDelegate3" } - ListElement { type: "SwitchDelegate"; value: "SwitchDelegate1" } - ListElement { type: "SwitchDelegate"; value: "SwitchDelegate2" } - ListElement { type: "SwitchDelegate"; value: "SwitchDelegate3" } - } - - delegate: Loader { - id: delegateLoader - width: ListView.view.width - // qmllint disable unqualified - sourceComponent: listView.delegateComponentMap[type] - // qmllint enable unqualified - - required property string value - required property string type - required property var model - required property int index - - property Loader delegateItem: delegateLoader - // qmllint disable unqualified - property ListView view: listView - // qmllint enable unqualified - property int ourIndex: index - } } } } diff --git a/examples/quickcontrols/gallery/pages/DialPage.qml b/examples/quickcontrols/gallery/pages/DialPage.qml index 17c9e090..2ccd1fa9 100644 --- a/examples/quickcontrols/gallery/pages/DialPage.qml +++ b/examples/quickcontrols/gallery/pages/DialPage.qml @@ -15,11 +15,12 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "The Dial is similar to a traditional dial knob that is found on devices such as " - + "stereos or industrial equipment. It allows the user to specify a value within a range." + text: qsTr("The Dial is similar to a traditional dial knob that is found on devices such as " + + "stereos or industrial equipment. It allows the user to specify a value within a range.") } Dial { + enabled: !GalleryConfig.disabled value: 0.5 anchors.horizontalCenter: parent.horizontalCenter } diff --git a/examples/quickcontrols/gallery/pages/DialogPage.qml b/examples/quickcontrols/gallery/pages/DialogPage.qml index ffabb415..157c6325 100644 --- a/examples/quickcontrols/gallery/pages/DialogPage.qml +++ b/examples/quickcontrols/gallery/pages/DialogPage.qml @@ -18,56 +18,58 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "Dialog is a popup that is mostly used for short-term tasks " - + "and brief communications with the user." + text: qsTr("Dialog is a popup that is mostly used for short-term tasks " + + "and brief communications with the user.") } Button { - text: "Message" + text: qsTr("Message") anchors.horizontalCenter: parent.horizontalCenter width: page.buttonWidth onClicked: messageDialog.open() Dialog { id: messageDialog + enabled: !GalleryConfig.disabled x: (parent.width - width) / 2 y: (parent.height - height) / 2 - title: "Message" + title: qsTr("Message") Label { - text: "Lorem ipsum dolor sit amet..." + text: qsTr("Lorem ipsum dolor sit amet...") } } } Button { id: button - text: "Confirmation" + text: qsTr("Confirmation") anchors.horizontalCenter: parent.horizontalCenter width: page.buttonWidth onClicked: confirmationDialog.open() Dialog { id: confirmationDialog + enabled: !GalleryConfig.disabled x: (parent.width - width) / 2 y: (parent.height - height) / 2 parent: Overlay.overlay modal: true - title: "Confirmation" + title: qsTr("Confirmation") standardButtons: Dialog.Yes | Dialog.No Column { spacing: 20 anchors.fill: parent Label { - text: "The document has been modified.\nDo you want to save your changes?" + text: qsTr("The document has been modified.\nDo you want to save your changes?") } CheckBox { - text: "Do not ask again" + text: qsTr("Do not ask again") anchors.right: parent.right } } @@ -75,13 +77,14 @@ ScrollablePage { } Button { - text: "Content" + text: qsTr("Content") anchors.horizontalCenter: parent.horizontalCenter width: page.buttonWidth onClicked: contentDialog.open() Dialog { id: contentDialog + enabled: !GalleryConfig.disabled x: (parent.width - width) / 2 y: (parent.height - height) / 2 @@ -90,7 +93,7 @@ ScrollablePage { parent: Overlay.overlay modal: true - title: "Content" + title: qsTr("Content") standardButtons: Dialog.Close Flickable { @@ -114,13 +117,13 @@ ScrollablePage { Label { width: parent.width - text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc finibus " + text: qsTr("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc finibus " + "in est quis laoreet. Interdum et malesuada fames ac ante ipsum primis " + "in faucibus. Curabitur eget justo sollicitudin enim faucibus bibendum. " + "Suspendisse potenti. Vestibulum cursus consequat mauris id sollicitudin. " + "Duis facilisis hendrerit consectetur. Curabitur sapien tortor, efficitur " + "id auctor nec, efficitur et nisl. Ut venenatis eros in nunc placerat, " - + "eu aliquam enim suscipit." + + "eu aliquam enim suscipit.") wrapMode: Label.Wrap } } @@ -137,13 +140,14 @@ ScrollablePage { } Button { - text: "Input" + text: qsTr("Input") anchors.horizontalCenter: parent.horizontalCenter width: page.buttonWidth onClicked: inputDialog.open() Dialog { id: inputDialog + enabled: !GalleryConfig.disabled x: (parent.width - width) / 2 y: (parent.height - height) / 2 @@ -151,7 +155,7 @@ ScrollablePage { focus: true modal: true - title: "Input" + title: qsTr("Input") standardButtons: Dialog.Ok | Dialog.Cancel ColumnLayout { @@ -159,16 +163,16 @@ ScrollablePage { anchors.fill: parent Label { elide: Label.ElideRight - text: "Please enter the credentials:" + text: qsTr("Please enter the credentials:") Layout.fillWidth: true } TextField { focus: true - placeholderText: "Username" + placeholderText: qsTr("Username") Layout.fillWidth: true } TextField { - placeholderText: "Password" + placeholderText: qsTr("Password") echoMode: TextField.PasswordEchoOnEdit Layout.fillWidth: true } diff --git a/examples/quickcontrols/gallery/pages/FramePage.qml b/examples/quickcontrols/gallery/pages/FramePage.qml index 85264425..0f4800f4 100644 --- a/examples/quickcontrols/gallery/pages/FramePage.qml +++ b/examples/quickcontrols/gallery/pages/FramePage.qml @@ -17,10 +17,11 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "Frame is used to layout a logical group of controls together, within a visual frame." + text: qsTr("Frame is used to layout a logical group of controls together, within a visual frame.") } Frame { + enabled: !GalleryConfig.disabled anchors.horizontalCenter: parent.horizontalCenter Column { @@ -28,17 +29,17 @@ ScrollablePage { width: page.itemWidth RadioButton { - text: "First" + text: qsTr("First") checked: true width: parent.width } RadioButton { id: button - text: "Second" + text: qsTr("Second") width: parent.width } RadioButton { - text: "Third" + text: qsTr("Third") width: parent.width } } diff --git a/examples/quickcontrols/gallery/pages/GalleryConfig.qml b/examples/quickcontrols/gallery/pages/GalleryConfig.qml new file mode 100644 index 00000000..7f230d9c --- /dev/null +++ b/examples/quickcontrols/gallery/pages/GalleryConfig.qml @@ -0,0 +1,9 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma Singleton +import QtQuick + +QtObject { + property bool disabled: false +} diff --git a/examples/quickcontrols/gallery/pages/GroupBoxPage.qml b/examples/quickcontrols/gallery/pages/GroupBoxPage.qml index 9e24d8e6..a3be0cfa 100644 --- a/examples/quickcontrols/gallery/pages/GroupBoxPage.qml +++ b/examples/quickcontrols/gallery/pages/GroupBoxPage.qml @@ -17,11 +17,12 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "A GroupBox provides a frame, a title on top of it, and a logical group of controls within that frame." + text: qsTr("A GroupBox provides a frame, a title on top of it, and a logical group of controls within that frame.") } GroupBox { - title: "Title" + enabled: !GalleryConfig.disabled + title: qsTr("Title") anchors.horizontalCenter: parent.horizontalCenter Column { @@ -29,17 +30,17 @@ ScrollablePage { width: page.itemWidth RadioButton { - text: "First" + text: qsTr("First") checked: true width: parent.width } RadioButton { id: button - text: "Second" + text: qsTr("Second") width: parent.width } RadioButton { - text: "Third" + text: qsTr("Third") width: parent.width } } diff --git a/examples/quickcontrols/gallery/pages/MenuBarPage.qml b/examples/quickcontrols/gallery/pages/MenuBarPage.qml new file mode 100644 index 00000000..a59f536f --- /dev/null +++ b/examples/quickcontrols/gallery/pages/MenuBarPage.qml @@ -0,0 +1,42 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +Page { + id: page + enabled: !GalleryConfig.disabled + + header: MenuBar { + Menu { + title: qsTr("&File") + Action { text: qsTr("&New...") } + Action { text: qsTr("&Open...") } + Action { text: qsTr("&Save") } + Action { text: qsTr("Save &As...") } + MenuSeparator { } + Action { text: qsTr("&Quit") } + } + Menu { + title: qsTr("&Edit") + Action { text: qsTr("Cu&t") } + Action { text: qsTr("&Copy") } + Action { text: qsTr("&Paste") } + } + Menu { + title: qsTr("&Help") + Action { text: qsTr("&About") } + } + } + + Label { + anchors.verticalCenter: parent.verticalCenter + width: parent.width + wrapMode: Label.Wrap + horizontalAlignment: Qt.AlignHCenter + text: qsTr("MenuBar provides a horizontal bar with drop-down menus, " + + "allowing users to access grouped commands and actions " + + "within an application.") + } +} diff --git a/examples/quickcontrols/gallery/pages/MonthGridPage.qml b/examples/quickcontrols/gallery/pages/MonthGridPage.qml new file mode 100644 index 00000000..bd99967b --- /dev/null +++ b/examples/quickcontrols/gallery/pages/MonthGridPage.qml @@ -0,0 +1,102 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Page { + id: page + enabled: !GalleryConfig.disabled + + Column { + spacing: 40 + width: parent.width + + Label { + width: parent.width + wrapMode: Label.Wrap + horizontalAlignment: Qt.AlignHCenter + text: qsTr("MonthGrid presents a calendar month as a grid of days, " + + "calculated for a specific month, year, and locale.") + } + + ColumnLayout { + spacing: 20 + anchors.horizontalCenter: parent.horizontalCenter + + RowLayout { + spacing: 10 + Layout.fillWidth: true + + Button { + implicitWidth: height + enabled: !GalleryConfig.disabled + flat: true + text: qsTr("<") + onClicked: { + const new_month = monthGrid.month - 1 + if (new_month < 0) { + monthGrid.month = 11 + --monthGrid.year + } else { + monthGrid.month = new_month + } + } + } + Item { + Layout.fillHeight: true + Layout.fillWidth: true + Label { + anchors.centerIn: parent + text: qsTr("%1 %2").arg(monthGrid.locale.monthName(monthGrid.month)) + .arg(monthGrid.year) + } + } + Button { + implicitWidth: height + enabled: !GalleryConfig.disabled + flat: true + text: qsTr(">") + onClicked: { + const new_month = monthGrid.month + 1 + if (new_month >= 12) { + monthGrid.month = 0 + ++monthGrid.year + } else { + monthGrid.month = new_month + } + } + } + } + + GridLayout { + columns: 2 + Layout.fillWidth: true + Layout.fillHeight: true + + DayOfWeekRow { + locale: monthGrid.locale + Layout.fillWidth: true + Layout.column: 1 + } + + WeekNumberColumn { + locale: monthGrid.locale + year: monthGrid.year + month: monthGrid.month + Layout.fillHeight: true + } + + MonthGrid { + id: monthGrid + locale: Qt.locale("en_US") + year: currentDate.getFullYear() + month: currentDate.getMonth() + readonly property date currentDate: new Date() + Layout.fillWidth: true + } + } + } + } +} diff --git a/examples/quickcontrols/gallery/pages/PageIndicatorPage.qml b/examples/quickcontrols/gallery/pages/PageIndicatorPage.qml index e83c8656..13620c12 100644 --- a/examples/quickcontrols/gallery/pages/PageIndicatorPage.qml +++ b/examples/quickcontrols/gallery/pages/PageIndicatorPage.qml @@ -15,7 +15,7 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "PageIndicator is used to indicate the currently active page in a container of pages." + text: qsTr("PageIndicator is used to indicate the currently active page in a container of pages.") } PageIndicator { diff --git a/examples/quickcontrols/gallery/pages/ProgressBarPage.qml b/examples/quickcontrols/gallery/pages/ProgressBarPage.qml index d712aae1..2a3f7158 100644 --- a/examples/quickcontrols/gallery/pages/ProgressBarPage.qml +++ b/examples/quickcontrols/gallery/pages/ProgressBarPage.qml @@ -15,8 +15,8 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "ProgressBar indicates the progress of an operation. It can be set in an " - + "indeterminate mode to indicate that the length of the operation is unknown." + text: qsTr("ProgressBar indicates the progress of an operation. It can be set in an " + + "indeterminate mode to indicate that the length of the operation is unknown.") } ProgressBar { diff --git a/examples/quickcontrols/gallery/pages/RadioButtonPage.qml b/examples/quickcontrols/gallery/pages/RadioButtonPage.qml index 644543c0..5358e6a3 100644 --- a/examples/quickcontrols/gallery/pages/RadioButtonPage.qml +++ b/examples/quickcontrols/gallery/pages/RadioButtonPage.qml @@ -15,8 +15,8 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "RadioButton presents an option button that can be toggled on or off. " - + "Radio buttons are typically used to select one option from a set of options." + text: qsTr("RadioButton presents an option button that can be toggled on or off. " + + "Radio buttons are typically used to select one option from a set of options.") } Column { @@ -24,14 +24,16 @@ ScrollablePage { anchors.horizontalCenter: parent.horizontalCenter RadioButton { - text: "First" + text: qsTr("First") + enabled: !GalleryConfig.disabled } RadioButton { - text: "Second" + text: qsTr("Second") checked: true + enabled: !GalleryConfig.disabled } RadioButton { - text: "Third" + text: qsTr("Third") enabled: false } } diff --git a/examples/quickcontrols/gallery/pages/RangeSliderPage.qml b/examples/quickcontrols/gallery/pages/RangeSliderPage.qml index 0ca23582..83dced76 100644 --- a/examples/quickcontrols/gallery/pages/RangeSliderPage.qml +++ b/examples/quickcontrols/gallery/pages/RangeSliderPage.qml @@ -15,17 +15,18 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "RangeSlider is used to select a range specified by two values, by sliding each handle along a track." + text: qsTr("RangeSlider is used to select a range specified by two values, by sliding each handle along a track.") } RangeSlider { - id: slider + enabled: !GalleryConfig.disabled first.value: 0.25 second.value: 0.75 anchors.horizontalCenter: parent.horizontalCenter } RangeSlider { + enabled: !GalleryConfig.disabled orientation: Qt.Vertical first.value: 0.25 second.value: 0.75 diff --git a/examples/quickcontrols/gallery/pages/ScrollBarPage.qml b/examples/quickcontrols/gallery/pages/ScrollBarPage.qml index 248e74ca..04bd8c77 100644 --- a/examples/quickcontrols/gallery/pages/ScrollBarPage.qml +++ b/examples/quickcontrols/gallery/pages/ScrollBarPage.qml @@ -6,7 +6,7 @@ import QtQuick.Controls Flickable { id: flickable - + enabled: !GalleryConfig.disabled contentHeight: pane.height Pane { @@ -19,13 +19,19 @@ Flickable { spacing: 40 width: parent.width + CheckBox { + id: alwaysOnCheckBox + width: parent.width + text: qsTr("Always on") + } + Label { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "ScrollBar is an interactive bar that can be used to scroll to a specific position. " + text: qsTr("ScrollBar is an interactive bar that can be used to scroll to a specific position. " + "A scroll bar can be either vertical or horizontal, and can be attached to any Flickable, " - + "such as ListView and GridView." + + "such as ListView and GridView.") } Image { @@ -36,5 +42,7 @@ Flickable { } } - ScrollBar.vertical: ScrollBar { } + ScrollBar.vertical: ScrollBar { + policy: alwaysOnCheckBox.checked ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded + } } diff --git a/examples/quickcontrols/gallery/pages/ScrollIndicatorPage.qml b/examples/quickcontrols/gallery/pages/ScrollIndicatorPage.qml index 04ce9748..e16d62c5 100644 --- a/examples/quickcontrols/gallery/pages/ScrollIndicatorPage.qml +++ b/examples/quickcontrols/gallery/pages/ScrollIndicatorPage.qml @@ -6,7 +6,7 @@ import QtQuick.Controls Flickable { id: flickable - + enabled: !GalleryConfig.disabled contentHeight: pane.height Pane { @@ -23,9 +23,9 @@ Flickable { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "ScrollIndicator is a non-interactive indicator that indicates the current scroll position. " + text: qsTr("ScrollIndicator is a non-interactive indicator that indicates the current scroll position. " + "A scroll indicator can be either vertical or horizontal, and can be attached to any Flickable, " - + "such as ListView and GridView." + + "such as ListView and GridView.") } Image { diff --git a/examples/quickcontrols/gallery/pages/SearchFieldPage.qml b/examples/quickcontrols/gallery/pages/SearchFieldPage.qml new file mode 100644 index 00000000..e790994b --- /dev/null +++ b/examples/quickcontrols/gallery/pages/SearchFieldPage.qml @@ -0,0 +1,58 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ScrollablePage { + id: page + + Column { + spacing: 40 + width: parent.width + + Label { + width: parent.width + wrapMode: Label.Wrap + horizontalAlignment: Qt.AlignHCenter + text: qsTr("SearchField is a styled text input for searching, typically " + + "with a magnifier and clear icon.") + } + + ListModel { + id: colorModel + ListElement { color: "blue" } + ListElement { color: "green" } + ListElement { color: "red" } + ListElement { color: "yellow" } + ListElement { color: "orange" } + ListElement { color: "purple" } + } + + SortFilterProxyModel { + id: colorFilter + model: colorModel + sorters: [ + RoleSorter { + roleName: "color" + } + ] + filters: [ + FunctionFilter { + component CustomData: QtObject { property string color } + property var regExp: new RegExp(colorSearch.text, "i") + onRegExpChanged: invalidate() + function filter(data: CustomData): bool { + return regExp.test(data.color); + } + } + ] + } + + SearchField { + id: colorSearch + suggestionModel: colorFilter + anchors.horizontalCenter: parent.horizontalCenter + } + } +} diff --git a/examples/quickcontrols/gallery/pages/SliderPage.qml b/examples/quickcontrols/gallery/pages/SliderPage.qml index fd03680a..b7d83fdf 100644 --- a/examples/quickcontrols/gallery/pages/SliderPage.qml +++ b/examples/quickcontrols/gallery/pages/SliderPage.qml @@ -15,16 +15,17 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "Slider is used to select a value by sliding a handle along a track." + text: qsTr("Slider is used to select a value by sliding a handle along a track.") } Slider { - id: slider + enabled: !GalleryConfig.disabled value: 0.5 anchors.horizontalCenter: parent.horizontalCenter } Slider { + enabled: !GalleryConfig.disabled orientation: Qt.Vertical value: 0.5 anchors.horizontalCenter: parent.horizontalCenter diff --git a/examples/quickcontrols/gallery/pages/SpinBoxPage.qml b/examples/quickcontrols/gallery/pages/SpinBoxPage.qml index 18c9b06c..4536e8a9 100644 --- a/examples/quickcontrols/gallery/pages/SpinBoxPage.qml +++ b/examples/quickcontrols/gallery/pages/SpinBoxPage.qml @@ -15,12 +15,12 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "SpinBox allows the user to choose an integer value by clicking the up or down indicator buttons, " - + "by pressing up or down on the keyboard, or by entering a text value in the input field." + text: qsTr("SpinBox allows the user to choose an integer value by clicking the up or down indicator buttons, " + + "by pressing up or down on the keyboard, or by entering a text value in the input field.") } SpinBox { - id: box + enabled: !GalleryConfig.disabled value: 50 anchors.horizontalCenter: parent.horizontalCenter editable: true diff --git a/examples/quickcontrols/gallery/pages/SplitViewPage.qml b/examples/quickcontrols/gallery/pages/SplitViewPage.qml new file mode 100644 index 00000000..b04e0bb7 --- /dev/null +++ b/examples/quickcontrols/gallery/pages/SplitViewPage.qml @@ -0,0 +1,73 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Page { + id: page + enabled: !GalleryConfig.disabled + + ColumnLayout { + anchors.fill: parent + spacing: 40 + + CheckBox { + id: orientationCheckBox + text: qsTr("Vertical") + } + + Label { + wrapMode: Label.Wrap + horizontalAlignment: Qt.AlignHCenter + text: qsTr("SplitView provides a container that arranges items horizontally " + + "or vertically, separated by draggable splitters, allowing users " + + "to interactively resize adjacent views within an application.") + Layout.fillWidth: true + } + + SplitView { + orientation: orientationCheckBox.checked ? Qt.Vertical : Qt.Horizontal + Layout.fillHeight: true + Layout.fillWidth: true + + Rectangle { + implicitWidth: 200 + implicitHeight: 100 + color: "lightblue" + SplitView.maximumWidth: 400 + + Label { + text: "View 1" + anchors.centerIn: parent + } + } + + Rectangle { + id: centerItem + color: "lightgray" + SplitView.minimumWidth: 50 + SplitView.minimumHeight: 50 + SplitView.fillWidth: true + SplitView.fillHeight: true + + Label { + text: "View 2" + anchors.centerIn: parent + } + } + + Rectangle { + implicitWidth: 200 + implicitHeight: 100 + color: "lightgreen" + + Label { + text: "View 3" + anchors.centerIn: parent + } + } + } + } +} diff --git a/examples/quickcontrols/gallery/pages/StackViewPage.qml b/examples/quickcontrols/gallery/pages/StackViewPage.qml index 841d491e..b8db9f76 100644 --- a/examples/quickcontrols/gallery/pages/StackViewPage.qml +++ b/examples/quickcontrols/gallery/pages/StackViewPage.qml @@ -9,6 +9,7 @@ import QtQuick.Controls StackView { id: stackView initialItem: page + enabled: !GalleryConfig.disabled Component { id: page @@ -25,21 +26,21 @@ StackView { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "StackView provides a stack-based navigation model which can be used with a set of interlinked pages. " + text: qsTr("StackView provides a stack-based navigation model which can be used with a set of interlinked pages. " + "Items are pushed onto the stack as the user navigates deeper into the material, and popped off again " - + "when he chooses to go back." + + "when he chooses to go back.") } Button { id: button - text: "Push" + text: qsTr("Push") anchors.horizontalCenter: parent.horizontalCenter width: Math.max(button.implicitWidth, Math.min(button.implicitWidth * 2, pane.availableWidth / 3)) onClicked: stackView.push(page) } Button { - text: "Pop" + text: qsTr("Pop") enabled: stackView.depth > 1 width: Math.max(button.implicitWidth, Math.min(button.implicitWidth * 2, pane.availableWidth / 3)) anchors.horizontalCenter: parent.horizontalCenter @@ -50,7 +51,7 @@ StackView { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "Stack Depth: " + stackView.depth + text: qsTr("Stack Depth:") + " " + stackView.depth } } } diff --git a/examples/quickcontrols/gallery/pages/SwipeViewPage.qml b/examples/quickcontrols/gallery/pages/SwipeViewPage.qml index 03958320..7b3e2622 100644 --- a/examples/quickcontrols/gallery/pages/SwipeViewPage.qml +++ b/examples/quickcontrols/gallery/pages/SwipeViewPage.qml @@ -11,6 +11,7 @@ Pane { id: view currentIndex: 1 anchors.fill: parent + enabled: !GalleryConfig.disabled Repeater { model: 3 @@ -27,8 +28,8 @@ Pane { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "SwipeView provides a navigation model that simplifies horizontal paged scrolling. " - + "The page indicator on the bottom shows which is the presently active page." + text: qsTr("SwipeView provides a navigation model that simplifies horizontal paged scrolling. " + + "The page indicator on the bottom shows which is the presently active page.") } Image { diff --git a/examples/quickcontrols/gallery/pages/SwitchPage.qml b/examples/quickcontrols/gallery/pages/SwitchPage.qml index cca20058..2dd4291b 100644 --- a/examples/quickcontrols/gallery/pages/SwitchPage.qml +++ b/examples/quickcontrols/gallery/pages/SwitchPage.qml @@ -15,8 +15,8 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "Switch is an option button that can be dragged or toggled on or off. " - + "Switches are typically used to select between two states." + text: qsTr("Switch is an option button that can be dragged or toggled on or off. " + + "Switches are typically used to select between two states.") } Column { @@ -24,16 +24,14 @@ ScrollablePage { anchors.horizontalCenter: parent.horizontalCenter Switch { - text: "First" + enabled: !GalleryConfig.disabled + text: qsTr("First") } Switch { - text: "Second" + enabled: !GalleryConfig.disabled + text: qsTr("Second") checked: true } - Switch { - text: "Third" - enabled: false - } } } } diff --git a/examples/quickcontrols/gallery/pages/TabBarPage.qml b/examples/quickcontrols/gallery/pages/TabBarPage.qml index d4dfeb89..08477298 100644 --- a/examples/quickcontrols/gallery/pages/TabBarPage.qml +++ b/examples/quickcontrols/gallery/pages/TabBarPage.qml @@ -6,6 +6,7 @@ import QtQuick.Controls Page { id: page + enabled: !GalleryConfig.disabled SwipeView { id: swipeView @@ -27,8 +28,8 @@ Page { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "TabBar is a bar with icons or text which allows the user " - + "to switch between different subtasks, views, or modes." + text: qsTr("TabBar is a bar with icons or text which allows the user " + + "to switch between different subtasks, views, or modes.") } Image { @@ -45,13 +46,13 @@ Page { currentIndex: swipeView.currentIndex TabButton { - text: "First" + text: qsTr("First") } TabButton { - text: "Second" + text: qsTr("Second") } TabButton { - text: "Third" + text: qsTr("Third") } } } diff --git a/examples/quickcontrols/gallery/pages/TableViewPage.qml b/examples/quickcontrols/gallery/pages/TableViewPage.qml new file mode 100644 index 00000000..e9ebfea6 --- /dev/null +++ b/examples/quickcontrols/gallery/pages/TableViewPage.qml @@ -0,0 +1,90 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt.labs.qmlmodels + +Page { + id: page + enabled: !GalleryConfig.disabled + + GridLayout { + anchors.fill: parent + + Label { + wrapMode: Label.Wrap + horizontalAlignment: Qt.AlignHCenter + text: qsTr("TableView provides a scrollable grid that displays data from " + + "a model in rows and columns, allowing users to view and interact " + + "with structured information within an application.") + Layout.fillWidth: true + Layout.columnSpan: 2 + } + + HorizontalHeaderView { + clip: true + syncView: tableView + model: tableModel.headerModel + Layout.column: 1 + Layout.row: 1 + Layout.fillWidth: true + } + + VerticalHeaderView { + clip: true + syncView: tableView + Layout.column: 0 + Layout.row: 2 + Layout.fillHeight: true + } + + TableView { + id: tableView + columnSpacing: 1 + rowSpacing: 1 + clip: true + + selectionModel: ItemSelectionModel {} + model: tableModel + + Layout.column: 1 + Layout.row: 2 + Layout.fillWidth: true + Layout.fillHeight: true + + delegate: TableViewDelegate { + implicitWidth: 100 + implicitHeight: 50 + Component.onCompleted: { + if (contentItem as Label) { + contentItem.horizontalAlignment = Qt.AlignHCenter + contentItem.verticalAlignment = Qt.AlignVCenter + } + } + } + } + } + + TableModel { + id: tableModel + property var headerModel: [qsTr("Name"), qsTr("Color")] + TableModelColumn { display: "name" } + TableModelColumn { display: "color" } + rows: [ + { + "name": qsTr("cat"), + "color": qsTr("black") + }, + { + "name": qsTr("dog"), + "color": qsTr("brown") + }, + { + "name": qsTr("bird"), + "color": qsTr("white") + } + ] + } +} diff --git a/examples/quickcontrols/gallery/pages/TextAreaPage.qml b/examples/quickcontrols/gallery/pages/TextAreaPage.qml index 3e9d7ee5..d4f3ca0f 100644 --- a/examples/quickcontrols/gallery/pages/TextAreaPage.qml +++ b/examples/quickcontrols/gallery/pages/TextAreaPage.qml @@ -15,15 +15,16 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "TextArea is a multi-line text editor." + text: qsTr("TextArea is a multi-line text editor.") } TextArea { + enabled: !GalleryConfig.disabled width: page.availableWidth / 3 anchors.horizontalCenter: parent.horizontalCenter wrapMode: TextArea.Wrap - text: "TextArea\n...\n...\n..." + text: qsTr("TextArea\n...\n...\n...") } } } diff --git a/examples/quickcontrols/gallery/pages/TextFieldPage.qml b/examples/quickcontrols/gallery/pages/TextFieldPage.qml index 2b06894c..ba61145e 100644 --- a/examples/quickcontrols/gallery/pages/TextFieldPage.qml +++ b/examples/quickcontrols/gallery/pages/TextFieldPage.qml @@ -15,12 +15,12 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "TextField is a single-line text editor." + text: qsTr("TextField is a single-line text editor.") } TextField { - id: field - placeholderText: "TextField" + enabled: !GalleryConfig.disabled + placeholderText: qsTr("TextField") anchors.horizontalCenter: parent.horizontalCenter } } diff --git a/examples/quickcontrols/gallery/pages/ToolBarPage.qml b/examples/quickcontrols/gallery/pages/ToolBarPage.qml new file mode 100644 index 00000000..b4b2cc8d --- /dev/null +++ b/examples/quickcontrols/gallery/pages/ToolBarPage.qml @@ -0,0 +1,74 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Page { + id: page + enabled: !GalleryConfig.disabled + + header: ToolBar { + RowLayout { + anchors.fill: parent + + Item { + Layout.fillHeight: true + Layout.preferredWidth: height + } + + Label { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: qsTr("Header") + + Layout.fillHeight: true + Layout.fillWidth: true + } + + ToolSeparator { } + + ToolButton { text: "\u2699" } + } + } + + Label { + anchors.centerIn: parent + width: parent.width - 20 + wrapMode: Label.Wrap + horizontalAlignment: Qt.AlignHCenter + text: qsTr("ToolBar provides a horizontal container for application-wide " + + "and context-sensitive controls, such as navigation buttons and " + + "search fields, typically used as a header or footer within an " + + "application window") + } + + footer: ToolBar { + RowLayout { + anchors.fill: parent + + Label { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: "\u2139" + + Layout.fillHeight: true + Layout.preferredWidth: height + } + + Label { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: qsTr("Footer") + + Layout.fillHeight: true + Layout.fillWidth: true + } + + ToolSeparator { } + + ToolButton { text: "\u2630" } + } + } +} diff --git a/examples/quickcontrols/gallery/pages/ToolTipPage.qml b/examples/quickcontrols/gallery/pages/ToolTipPage.qml index dd92c89b..9a6cfc57 100644 --- a/examples/quickcontrols/gallery/pages/ToolTipPage.qml +++ b/examples/quickcontrols/gallery/pages/ToolTipPage.qml @@ -15,16 +15,16 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "A tool tip is a short piece of text that informs the user of a control's function." + text: qsTr("A tool tip is a short piece of text that informs the user of a control's function.") } Button { - text: "Tip" + text: qsTr("Tip") anchors.horizontalCenter: parent.horizontalCenter ToolTip.timeout: 5000 ToolTip.visible: pressed - ToolTip.text: "This is a tool tip." + ToolTip.text: qsTr("This is a tool tip.") } } } diff --git a/examples/quickcontrols/gallery/pages/TreeViewPage.qml b/examples/quickcontrols/gallery/pages/TreeViewPage.qml new file mode 100644 index 00000000..b7ee2b27 --- /dev/null +++ b/examples/quickcontrols/gallery/pages/TreeViewPage.qml @@ -0,0 +1,113 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt.labs.qmlmodels + +Page { + id: page + + GridLayout { + anchors.fill: parent + anchors.margins: 10 + + Label { + wrapMode: Label.Wrap + horizontalAlignment: Qt.AlignHCenter + text: qsTr("TreeView provides a hierarchical view for displaying and " + + "navigating tree-structured data, allowing users to expand and " + + "collapse nodes to explore parent-child relationships within a model") + + Layout.fillWidth: true + Layout.columnSpan: 2 + } + + Item { + implicitHeight: 40 + + Layout.columnSpan: 2 + Layout.row: 1 + } + + HorizontalHeaderView { + clip: true + enabled: !GalleryConfig.disabled + syncView: treeView + model: [qsTr("Location")] + + Layout.column: 1 + Layout.row: 2 + Layout.fillWidth: true + } + + VerticalHeaderView { + clip: true + enabled: !GalleryConfig.disabled + syncView: treeView + model: Array.from({length: treeView.rows}, (v, k) => k + 1) + + Layout.column: 0 + Layout.row: 3 + Layout.fillHeight: true + } + + TreeView { + id: treeView + clip: true + enabled: !GalleryConfig.disabled + rowSpacing: 2 + model: treeModel + + Layout.column: 1 + Layout.row: 3 + Layout.fillWidth: true + Layout.fillHeight: true + + selectionModel: ItemSelectionModel {} + delegate: TreeViewDelegate { } + + columnWidthProvider: (column) => column === 0 ? treeView.width : 0 + + Component.onCompleted: expandRecursively() + } + } + + TreeModel { + id: treeModel + + TableModelColumn { display: "location" } + + rows: [ + { + location: qsTr("America"), + rows: [ + { location: qsTr("Brazil") }, + { + location: qsTr("Canada"), + rows: [ + { location: qsTr("Calgary") }, + { location: qsTr("Vancouver") } + ] + } + ] + }, + { location: qsTr("Asia") }, + { + location: qsTr("Europe"), + rows: [ + { + location: qsTr("Italy"), + rows: [ + { location: qsTr("Milan") }, + { location: qsTr("Rome") } + ] + }, + { location: qsTr("Portugal") } + ] + } + + ] + } +} diff --git a/examples/quickcontrols/gallery/pages/TumblerPage.qml b/examples/quickcontrols/gallery/pages/TumblerPage.qml index 4d01f02e..471553a6 100644 --- a/examples/quickcontrols/gallery/pages/TumblerPage.qml +++ b/examples/quickcontrols/gallery/pages/TumblerPage.qml @@ -15,10 +15,11 @@ ScrollablePage { width: parent.width wrapMode: Label.Wrap horizontalAlignment: Qt.AlignHCenter - text: "Tumbler is used to select a value by spinning a wheel." + text: qsTr("Tumbler is used to select a value by spinning a wheel.") } Tumbler { + enabled: !GalleryConfig.disabled model: 10 anchors.horizontalCenter: parent.horizontalCenter } diff --git a/examples/quickcontrols/gallery/qmldir b/examples/quickcontrols/gallery/qmldir index 6b7f86bf..212f9919 100644 --- a/examples/quickcontrols/gallery/qmldir +++ b/examples/quickcontrols/gallery/qmldir @@ -1 +1,2 @@ module App +singleton GalleryConfig 1.0 pages/GalleryConfig.qml diff --git a/examples/quickcontrols/gallery/rc_gallery.py b/examples/quickcontrols/gallery/rc_gallery.py index 786c9cfb..c7107ae2 100644 --- a/examples/quickcontrols/gallery/rc_gallery.py +++ b/examples/quickcontrols/gallery/rc_gallery.py @@ -1,170 +1,201 @@ # Resource object code (Python 3) # Created by: object code -# Created by: The Resource Compiler for Qt version 6.5.0 +# Created by: The Resource Compiler for Qt version 6.10.0 # WARNING! All changes made in this file will be lost! from PySide6 import QtCore qt_resource_data = b"\ -\x00\x00\x09\xc1\ +\x00\x00\x0b\xbc\ \x00\ -\x00)\xbbx\xda\xc5Z\xdds\xdb6\x12\x7f\xd7_\x81\ -\xb0/RcQ\xb6s\xc9\x03\xdb\xdc\x8d-'\x8d\xe7\ -\x9cKb\xf9\x9a\xcet\xfa\x00\x91\x90\x843\x04\xd0\x00\ -h[\xcd\xf9\x7f\xbf\x05\xf8!~\x00\xb2\xc4I\xe78\ -\x93\x98\x04\x16\x8b\xc5b?~Xh2AS\x91n\ -$]\xae4\x1aNG\xe8\xf4\xf8\xf4\x04\xdd\xac\x08\xfa\ -\xa2\xa1g\x9db\xbeAW:\x09\x07\x93\x09\x9a}\xbe\ -\xf8m|Ec\xc2\x15\x19_&\x84k\xba\xa0DF\ -\xa8h\xbb&\x8b\xf1\x17=\x86ak\x22c\x8a\x19\xfa\ -t\x8d\xceg\x17\xe3W\xe3)\xc3\x99\x22\x83A*\xf1\ -r\x8d-g\xc1\x81\xc19Y\xe1{*\x80\xc7\xb9\xc8\ -x2\x18P\xe8\x90\x1af\x9f\x0aI\xb6__2\x1a\ -\xdf\xb6>\xc3+\xbc\x11\x99V\xed\xe6\xa9\xe0Z\x0a\xa6\ -*fA\x18 \xac\xd0Y\x9a\x0e\x06\xf0\x1f\xa31\xd6\ -T\xf0\xaf\x94'\xe2\x01}\x1b xh\x12\xa1\x07\xdb\ -`?\x1fh\xa2W\x11z\xf5\xe6\xd8~\xae\x88QP\ -\x84^\x9f\xe6\xdf\xf7T\xd19#\x11\xd22#\xb6E\ -Sm\xbe\x03P\x9b\x95\x02\x95R\x04\x03\xdb?\x99\xbc\ -@\xbf\x0bIa\xcdv\xf2?l\xab$8\x11\x9cm\ -P*EJ\xa4\xde\xa0\xb9\x10\x0c\x19\xa9%\xa6\xfa\xa3\ -HH)VhEB?\x97\x9f\xb9H\x1e\xde\xb6y\ -\x91\xf1\xd8|\x82\xf4,\x1d\x8e\x8a\x85\x9a\x87\x11\x8d\x12\ -\xaaR\x867\x94/\x0bI\xd1[\xc4\xa8\xd2\xbfR\xf2\ -\x10\xc6\x99\x94\xc0\xed\x92'\xe4\x11\xbdx\xfb\x16\x8dO\ -\x1a\x83\x8b\xfeb\xe4\xbf\xf0\x9a\xc0\xe8\x0e\xc7j\x88y\ -\xfe\xb1\xe5\xbe\x86e\xb1pI\xf4\xd09\xe1(\xb4\xca\ -\x0c\xb5\xb8\x12\x0fDN\xb1\x22 =\xe86h\xc8\x90\ -I#r\xb0\xd2:U\xd1d\x92\x888\xbc\xd3!\x15\ -\x93;=~3\x09\x1a\x93\xbfD\xc3\xdd\xd2Y\x01\x83\ -\xbb5\x1b\xdf\xe9;\xb3\x7f\xe3\xb8\xd8\xbf\xd3q\x00\xc3\ -\x1d\x0b~\x09v\xb5\xd2k\x16\xb4\x19\x81\xa4]&\xc0\ -\x1a\x96\x9d\xc1\xb2\xec\x98\xd1O\xd5\xa8/:\x84\xbd\xe7\ -\xff\x96\xec\xdd\xa3&\x92c\xc66CX\xdc\xc8R<\ -\x0d\x0aC\x01\x86\x92$[C\xb9\xc7\x12\xcd3\xca@\ -g3\xbdaD\xe5\x843\xa25\xacQ\xd5v\xdb\x98\ -\xb6*\x9a\xab\xc6\x8a\x8f\xd2\x12:\xe0\x0f\xf0\xa8\xcf8\ -[\x81\x11\xc6\x99\xae1R \x04\xe11Q\x11\xfa=\ -x\xa7\xe2\xe0\x08\x05\xe78\xbe\x0d\xfe\xa8H\x08\xc7\xe0\ -\x17fB\x0d\x1dvk\x13\x92\x82\xdd\xfe\x1dm-H\ -\xf030\xcc{\xac\x0d!\x07\xff_\xc2\xabatf\ -\xed5\x04\x89\x96K\x22\x87\xa3=\xe5\x89\xd0Lc\x9e\ -`\x99\xfc\x93l\xc2\x0f`\xedf\x9f`\xbb\x94{\xca\ -\xca\x83\x8c[\xd4\xe7\xc8\xe7o\xa9\xae+\xe0\xb6\x1b\xf6\ -7\xe4`\x0c\xce\xf5\x1a\x8b\x9a\x1b\xf5\x18\x8bH$\x06\ -[\x0ej\x02\xdd\xe4\xab4\x02}k\x98\x10]\xa0\xa1\ -\x83\xdd\xa8Ef5P\x91\xa5\xa2\x5cK\xfdq;t\ -\xc3\x9d\xed\xf2\x11a\x8a8\xf8\xe7R[\xfblq\x7f\ -\x1a4\xdf\xf6\xd9\xa5\xe0#\xe1Y\xe0\xde\x13\x91\x1a\xcd\ -*C\xb1\xcb\x0a\x9c;\xd4\x19\xeb\xda\xa0`\xdd\x9a\xbc\ -\xa6\xff\x1a\x83\xfaZ\x8b9W\x10\xa4M\x96\x83\xd4\x11\ -\xde@x>\x07\xcf\xdb\x0ap-\x1e\xf2<\xd4R\x9f\ -Jq\x0c\x8e\x15\xa1\x22e\x94\x0f\xe61\xe8H\x85\x0b\ -\xcaX\x84Rl\xb6\xc5I\xc0\xc8B\x7f\xc4rIy\ -\x84^\x14&[O\x0c`^\xc5\xf6\xe4\x99!B\x90\ -@\xc9\x82r\x92\x0c\x1a\x0c\xad\xcc\x99\xd6\x0d\xbdU\xb3\ -Y\x85\xed\xb4\xf2\xf2\xa9\x92\x9eC\x98\x96m4>\xaf\ -\xf0\x9c0\xc7\xd4f\xebl\xa4\xb7\x04\x9dnM\x1e!\ -\xe3v,X\x935\xac|\xe8l\x87$o\xfe^\x10\ -F\xccZ \x91\x00\x0f\xe3}\xbf@P%r\xd3\x09\ -\xd5h\x01\x81\x22L\xe9#a3\xfa'\xe9l\x96\x0d\ -i\x8c\x9a,l\x85\x0c\xdf\x99\x8f\xeb*\xf3\xd6\x1f\xd8\ -4\xfa'\xb0\xc3\xec\x8c\xd1%\x07s\x03\xf1!\xb8\xdb\ -\xaf\x0fS\xf8$\xb2\xabS\x88\xc1\x00G\x5cC~\xf5\ -\x0c\xc9\xad\xcd\xda\xcf\xd7\x1c\xa5T \xc4\xb3\x01{\x19\ -@\xd7\x89:\xa4\xa6\xcf\xc1\xc1\xe1\x86N\x9a\xc7\xd2\xdc\ -\x0b\x83\x1d\xe7(\xcbI\x0b\x86\xc5\xd5B\xc8\xf5'\xf0\ -R\xe3\x01\xd67oD\x9a\xeb\xde9\xa6\x13\x1b\xdc\x16\ -\x15\x94)2\xf0\x126\x82C\x99:/\x00\xd0\x8a\xa5\ -+\x16vcb\x1f\xb1L\xde\xdaS\xa4n\x02\xfb\xbe\ -\xa2\x9c\xcd\xc1\xc2\xf6\x94\x05\x1b\xda>\xbay\xda'\x9f\ -\x5c\xd8\x08\xd7\x8a\xf7y\xd8\xdb\x1aA\x01\xd6?b\xbd\ -\x0a\xd7\x94\x0f\xeb\x80\xf9\xa8\x89\x97Gh\x82^\xa1\x1f\ -\xd1i5\xb8\x84\xf6]Xm\x1e\xc0l\x98\xed\x8ex\ -\xd4x\xa9\xf1\xa2{wh4\xd1\xaa\x9d\xce\xdf\x02\xa4\ ->1\x98v\x81!\xf1nA\x99P4\xf7F7\x9f\ -c\x18\xb1\xcd\xdcU\x9c\xb1\x19\xf1\x9c\xd35hzL!\ -\xc5R\x12\xa5\x00O\xfb&\xd8R\x1c\xce\xfe\x1a'T\ -\xec4\xe6\x1aE\x1f\xf6|If\x06\x0eJ/\xfb\x8a\ -\xe2p\xf6\xb3X\x0a\xc6\xfc\xaa\xa9\xfa\xfb\xb2~ns\ -[T=\xa6\xd9\xa5\x9b\xdejI)\xf7\x9b{\xd1\xdb\ -\x83m\x99\x9f}\x8c\xcb\xfe\x1e\xac\x1fhJv\xb1.\ -\xfb{\xb1\xd6\xf1\xca\xcf\x17:\x0fgz\x83\xe7~\xab\ -\xcb;{0\x05\x5cy&\x09\xf6\xb1-\xba\xfb1~\ -O\x09Kvp\xb6\xfd=X\xc3\xa9\xe9\x86\xa6>\xc6\ -yo\x0f\xb6\xd9\x1a@\x9bW\xc1y\xaf\x8fm\xeb`\ -\x97\x14\xa9(j\x1c{=G\xed\x92\xd8\x90v\x08\x0a\ -\x08]b\xc5\xf0\xde\xfc\xe7>\x9e\xe5g\x04\xbb\x9c\xee\ -\x19\x18\xc033\x00\xda\x9c\x0c*fTM\xb7\x87\xf4\ -\xee\xd9\xad[\xdb\x04H\x0d\xff\x00f\xeeAk\xea\xa0\ -\x16Z\xeeA[\xd4:\xdd\xb2{\xc9\xf3}\xea\xca-\ -\xf8\x94\xd1\xf8\xb6[\xc0{\xae\xfa\xe6^Z\xab\x9e\x97\ -\xa9\xd50\x9f\xd9}\xa02UB\x07\xee\x1fyOm\ -E\xb1(f\xc2\xd4\xd3\x9f=\x8e5>[\xc9 ,\ -\xcb\x16Q\xbb\x07\x0c\xddW\x19,\x97\xd6\xaeL\x97\xed\ -\xdb\x19w\x96\xc8\xbeky\x8cr8oaf\xec2\ -B\x9f1'\x8eCR\x0a\xcdMm\x5c\xae\xc1==\ -^\x06\x90S\xf8\xbc\xcbp\x0a\xf1=\xa6\xcc\x14\xc9m\ -\xf1\x06\x8e\xa3\xa7]/*\x8e\xa4M\xfa\x0f\xb6\xd59\ -\xa0\xd4Il+F\x97\xdcYW\xac\x13\x96\xfb\x97\x97\ -\x98>-\x16\x8a\xc0|\xe3\xd7\xdd\xea\x97\xd9\x83\xfc\x1a\ -\xca\xae:\xfc\x0cx\x8c\xc8{r\xa6R\x12Ch\xed\ -\xceQ\x855\xba\xb6!\xedN\x8f\x8dR\xc2\x94/\x83\ ->e\xc2\xa2&\xd1\xb9X3~z\x0f(B!l\ -j4H,Py\xdd\x82\xf4\x0ak\x14c\x8e\xe6\x04\ -e\x0a|Z\x0b{Y\x92\x00\xc9:e\x04\x82\xa4=\ -\xb6/p\x0c\xe3)G%\xf70\xf0\xealm\x8dM\ -9k\x84%\x8d\x16in\x02\xe1\x5c\x00\xac\x5c{\x09\ -\x8d\xf5V\xe50\xf3\xe1\xa5\x94\xa5-XR\xe9,=\ -\x96\xb4\xf9\xa4\x11\xc2R\x827\x800\xfb\x15)\xf3\xd2\ -\xe6\xa1u\xca\xda(_\xa9\xf2A\xe247\x9e\x9c\xf8\ -+|\xef2\x81]\x8ee\xd7\xf4\xac\xb1\xe5+\xef\x98\ -Z?\xd5\x97\xea,h=[z@Y\xdc]\xe2\xb2\ -\xe7T\xcf}]\xde9\xa8\x95Pm\x95K\x9a\xcb\xf2\ -a\xa3\xd0U\x96SM\x85\xebt\x1b\xde7\x8d\x11\x8d\ -\x22\x17\x10\xbe\x199\x8bh9\xf1A\xf5\xb4Q\xbbd\ -\xd6(\x1f\xb9JJ\x15\x8a\xad\xca\xb1\x83Z\x1a\xb4\xd7\ -y\xf9\xd9\x0c\x06\x16\xf5\xc5O\xb7\xe8\xbf\xe5\xfb\x14v\ -\xa9\x96\xf4\xcdeRLR\xdd\xcd\xc6\xa5*C{\xc5\ -\x09\xe9\xd7\xfe\x85cBX\x5c\x09\x1b\x8c\xe8\x1cQ\xcc\ -\xd4\xce\x98O\xb5I\xaf\xc9\x7f \x0c:&-\xe7h\ -\xa5\xfe\xaa\xdd\xbe\x5cv\x80\xc0\xb3SW\xafq~\xc5\ -\x99\xa7\xae\xa9`\xd9\x9a;\xef\xa1\xea\xd6\x94\x93y\xaf\ -\xa9\x1a\x1d\xbe{\xad\xc6\xa0\x93\xe3\x81\xe3~\xc2\x1d\xc7\ -\xeb\x15x\xb3\xf8(p`\x90NSY\xcc\xdaq\xef\ -P\xea\xd4I\xd0\xc0\x94[\xa5w\xea\x97\xad*ea\ -\xe4\xcd\x0bv\x17}\xf5K\x96\x10\xe0`\x91[\xbc\x80\ -\xb0\xb2\x8c\xd2\x1c\x00\x8e$\xc3\xa6\x81\x1e\x99\x0b p\ -\xbdx\xf5\x9e>\x92df1\xa8\x1f\xd5\xe5\x97\xc5\x15\ -\xcb\xfc\xb7\x1a~\xf2v\x1d\xb74\xc9K/$u\xdf\ -$\xecq\x09\xf5,\xa8|&\xe1_\x13\x08\x03RW\ -\xb8\xbck.\xb1`\xe6\x97C\xc1\x0f\xe4o'\xe4\xf4\ -u\x97@\x18;\xd5\x9b\xc8\xe3\x8eFY\x0e\x87\x04\xe4\ -x\x12\x9aR\xfbqx\xfc\x7fN\xa0{\xaa\xb9F\xf6\ -\xa1\xc0\x8e\x8e;\xc1\xfd\xb3O\xedN\xa7wXo\xdd\ -!\x81\xc7\xf9\xd3U=[uR\xd4\xf7\xb9\xe6)\xc2\ -e\xa9\x1e\xbb\xc2<\x1c\x96W>\x83\xadK\x9bfG\ -\x18\xad\x0d\xda/\x86\xfa,\xbcXI\xfd\xea\xacy,\ -\xf0\xf9D\xf1\xc3\xbc\x16\x10\xce\x7fXd\x0e\xf8\x14\xcc\ -\xcc\xa0_\x82\xb8\xb9\xf9^\x12N\xa4\xfdI\x98A\xc1\ -r\x8by\xb7Hy\x8e\x0d<\x16;\x11\xf0\xb30\xce\ -u\x93~r\xda\xc7\xf7{k\xe6\x92[h\x8f%U\ -\xb0\x18\x80\xfb]5\x9d\x1c9\x1a\xdd\xb7\x9c/Q\x00\ -\x98\x0f\x80 \x122\x01\xc5\xc1\xd9\x02\xf0%\x1c\x163\ -8X*j\xc2\xbc\x8f\xd9\x94h\x991\xf3\ +Es\x8d\xefW\xeaB\x8e\x86 ;\xc3\x0a\xb1\xac\x0c\ +\xf7\x06\x06j2\xf9\x85|\x15\x92\xc3\xcc\x0d\x0b\xdf\xcc\ +W\xc9h$\x92xCR)R&\xf5\x86\xcc\x85\x88\ +\x09\xf2.)\xd7\xefE\x04\x98\x7f\xa9\x0c\x9b\xadXx\ +y$n\x83\x10\x1fXD~\xfe,\x98\x0f\x0c\xe3\xe4\ +\x1f\xf65g\xdcC\xdb|^dI\x88\xaf0\xc78\ +\x1d\xed\x15\xe2\xc0\x163M\x22\xae\xd2\x98nx\xb2,\ +\xa6B^\x91\x98+\xfd\x07g7A\x98I\x09\xd8N\ +\x92\x88\xdd\x92_^\xbd\x22\xe3\xfd\xda\xe0\xa2\xbf\x18\xf9\ +o\xbaf0\xba\x85\xb1\x1c\x82\xed\xf7-\xf65L;\ +\x0e\x96L\x8f\x9c\x04\xf7\x02#\xf2@\x8bSq\xc3\xe4\ +\x8c*\x06\xdcO\xc9pX\xe3!\x93\xc8\xf2p\xa5u\ +\xaa\xa6\x93I$\xc2\xe0J\x07\x5cL\xae\xf4\xf8\xe5d\ +X#\xfe\x84\x8c\xba\xb93\x0c\x0e\xaf\xd6\xf1\xf8J_\ +\xe1\x02\x8f\xc3b\x81\x0f\xc6C\x18\xee\x98\xf0\x13\xd0\xbe\ +\x95^\xc7\xc3&\x22\xe0\xb4\x8d\x04P\xc3\xb43\x98\x96\ +\x19\xb3\xf7[9\xea\x93\x0e@7\x92\xcf2~}\xab\ +\x99Lh\x1coF0\xb9=\x03q7(\x14\x09\x10\ +J\xd0\x87R\x91\xae\xa9$\xf3\x8c\xc7 \xb3s\xbd\x89\ +\x99\xca\x01\xcf\x99\xd60GUYm\xdc\x00\xaa\xf8\x5c\ +~,\xf1(-\xa1\x03~\x00G\x95\xe2\xf9\x0a\x944\ +\xcct\x05\x91\x02&X\x1225%_\x87\xafU8\ +|J\x86G4\xbc\x1c~+AXBa\xf7 A\ +\x0d\x1dfi#\x96\x82\xde\xfe\x93l5H$\x87\xa0\ +\x98\xd7T#`\x02Vb\x09\x8f\x88\xe8\xd0\xe8k\x00\ +\x1c-\x97L\x8e\xf6\x1e\xc0\xcf\xb9\xa6IDe\xf4/\ +\xb6\x09\xde\x81\xbe\xe3J\xc1\x82\xa9on\xaa\xe5&\xc2\ +\x9dQ%\x93\xb3\xd0\x90^\x9b\xc7m7,q\x90\x80\ +>8\xa7\x8cJ5G\x09\xa1RD\x92\x82:\x0f+\ +\x0c]\xe4\x13E\x86~\xd4\xb4\x88/\xc8\xc8\x81n\xaf\ +\x01f\x84P\x82\xa5\xc2\xce\xa5\xda\xdc{\xba\xb6\xa3\xcd\ +\xf4\x09\x8b\x15s\xe0\xcf\xb96*\xda\xc0~7\xa8?\ +uIP\xa4\xf8Q\xbdgI\xe6\x17\xe0p\x0d\xdd\x1e\ +\xf9T\x10Ty)h\xae\xc0\xce\xa2\xbb\x02\x1f\x10\x5c\ +\x80\x85=\x82\xcd\xb1e\xe0L\xdc\xe4\x0e\xa51=\x95\ +\xd2\x10t\x7fJ\x0a\xdbo\x1bMBP6\x15,x\ +\x1cOIJQlN\x80\x98-\xf4{*\x97<\x01\ ++^\xa8T\xd5\xb6\xc3\xf2\x17\xe2\xcb\x8d\xf7\x94\x80'\ +d\x0b\x9e\xb0hPChx\xce\xb4\xae\xc9\xad\xa4f\ +\x04\xd6\xa9\x85\xb6\x95\xde\xcb\xc1Lc\xedj\xaf\xa7t\ +\xceb\x07i\x5c:c\x8c\x0d@\xab[\xb3[p\x9d\ +-\x0d\xd3l\x0d3o\x1bw\xfc\x0e\xde\x1a\x7f\x8fY\ +\xccp.`\xeb\x01\x07\xb1\xee\xf5-\x18?&7\xc3\ +\xb6\x1a/`?\x07)\xbfe\xf19\xff\xceZK\x86\ +\x8d\xc5\x1c\xdd\xa9a5x\x8d/g\xa5\x8b\xac6X\ +:\xfe\x1d\xd0\xd1\xf80\xe6\xcb\x04\x94\x0e&\x01V\xd8\ +\xbc\xbd\x9b\xc1+\x93m\xc9\x82\xb1\x84\xe8\xc25\xe4\x0f\ +\xcf\x90\x5c\xe7\x8c\x16}\xc9\x83\x8e2\xa6\xf0,\xc3N\ +j\xd0\xdeJ-P\xecs`\xc0\xd6\xd8\x8cN\x98[\ +\xab\xf4\x85\xda\x8e\xf3\xa0\xc9\x09\x0b\xea\x95\xa8\x85\x90\xeb\ +\x0f\xb0Wq\x1f\x98\x1dz!\xd2\x5c\xf6\xce1-\x0b\ +\xd1Bj\xf4*W\x09\xeb\xd0\x1c:a[\xcdPX\ +Ow\x0cQ\xaaX\xba\xec\x96mw\x8fg\x0e=\xcd\ +\xae\x8c\xb5]\xce_\xc0\xd0\xe1\x1cTnW\x8e(\x02\ +\xf7\x91\xd3\xdd\xc0\xfdV\xf3\x03\xc7\xc6\xf25\xfc@n\ +\x0e\xb7jQD\xe3\xef\xa9^\x05k\x9e\x8c\xaa\xb1\xee\ +\xd3z\xa8\xbbG&\xe49\xf9\x95\x1c\x94\x83m\xec\xde\ +\x8e\x88\xb1A\xb8E\xe3nK\xc8q\xdf\xe2\xbe\xbav\ +\x9bL\xb4bM7\xfc\x0a\xa2\xe1}\x0cG\x17\x14\x1c\ +\xe66\x9e\x12\x8a\xe7\xfb\xd3\x8d\xe7\x19\x8c\xd8z\xdc\xd2\ +N\xbb\xbc\xc6V:\xa7\x85\x05mF\x07\xd1\xd6\xe8\xd6\ +w\xd8B\x84\x99r\xd8\x99\xaa\xef\x9f6]\xbf\xd3\xdd\ +\xd5 L\xc0>5\xec \x83.O\x81}\xafc\x86\ +\x96\x91\xfc\xa8\xe7LG\x99\xda\x00e\xcc\xcd\x84\x84\xc8\ +\x97(\x91\xc9\x10\xfd\xfd\x95\x0c\xa7\x93\x94.\x99\x9a\xd4\ +\x80>\xc2\xa7\x00\x22\xe6\xa1C\xf7:\x09\xa1\xe9\xf4S\ +\xc0\xde\x9e\xa8m^\xe6Cn\xfb\xfb\xa2\x17\xeb\xb9\xe8\ +B_\xf4\xf7D\x0f\xde\x96n\xba\xa5S\x01\xe9K\x04\ +\x0c\x89\x17;\xf4=\x02\xadXv!\x16\xcb\xfeb1\ +A\x88\xea\x10\x8a\x01\xe8\x89\xff\x8d\x84\xb0\xd6\x87\xdbt\ +\xf6D\xfcV\x8a,\xedP\x17\xdb\xdf\x13=\xbap\x88\ +\xa0}\xd8\x8b\xee\xbe\xc8!\xf2Z\xbd\x95<\xf2\xa2\xb7\ +\x00=\x09\xe0\xb0{\xcdM\x0d\xa8/!)\x96\x92)\ +\xd5!\xaa\x0aHO\x22g4\xe2\xa2{\xebV@z\ +\x13I\x96\xec\x1c#f\xefL* =\x89\x9c\x87R\ +\xc4q\x87\xb0J\x80G\x11\xb8w\xe9\x1b`}\x891\ +*\xc3\xd5\x1b\xceb\xaf\x22W@\xfa\x12\xe9\x5c\x92\xc7\ +\xadF\xca\x93\x0e\x0bRt\xf7F\x1es\x13\x9d\xf8\xd1\ +\x17\x00}\x09\xd8\xa0\xccK\xc0\x02\xf4%p\xc3S\xd6\ +I\xc0\x02\xf4'\xa0\xc3U\x07v\xe8\xed\x89\xfa\x82\xce\ +;vY\xde\xdb\x1fu\xdc)\x96\x12\xa0/\x01\xc8e\ +\x0e%\xa3^\xfcE\xff#\xd0w\xee\xd9\x12\xa0/\x81\ +\xbc\xf8\xe4E\x9fw?\x02\xf9\x05O\xbb\x90Cw_\ +\xe4\x92u\xafl\xd1\xdf\x17}\xb6\x06\xcd\xf0\x0b&\xef\ +\xf6!o\xd4I\xa2\x22.\x9b\xd6jI\x9e\xfa\x95\x05\ +F\xd0\x16@\x91\x7f\xdaD+\xb8\xc6?\xeejG\x9e\ +f\x9bI\xb5KJ\x90y\xc6\x98}bZ]\x22\xe3\ +j\xb6\xad|\xb5K!\xed\x9a>\xe4\xa3\xf0\x0fr\xb4\ +\x1d`\xb1\xfeo\xf2\xb2\x1d`\x8b\x1a\xbf\x9bw/x\ +\xbeNm\xbeE2\x8b9\x1eN5\xab\xd6\xb6\xf9\xaa\ +\xd7\xde\xaa\x84d:\x93\x8e:\x166_\xfd\xda-'\ +l\x95\x8ax\xa6V\xa3|\x1an\xe2\xc8\xa9#\x03\xf7\ +sZ\x94s\xc3X\xe0\xa1\xd4\xbd\x85\x91\xdak#\xf0\ +\x08lIq\xda\xec\x81\xbd\xe3\xa9\xa9\x94n\xady\xbc\ +c\xbfo)v\x16\xb1\xff\xd4\x026O\xb8\x864\x0c\ +\x95|J>\xd2\xa4\xb9\x11\x91\xc1\x14>\xd7\xa5q\xb2\ +\x86\xbd\xee\xd9\xb2\x90\xd2\x09\xdfVEL\x01\xbd\xa6<\ +FWc\x0a\xabdR)\x0a\xd9f\x8bCu\xf8w\ +\xe6\xabs\x80\x95Ih\xaa\xb9'\x89\xb3\xf2_\x05\xb4\ +\xeb\x97\x97\x7f?,\x16\x8a\x01\xbd\xf1\x8bve\x1a\xd7\ + ?\xeb5\xb3\x0e>BN\xc0\xe45;T)\x0b\ +\xc1\xdf\xb4i\x946\x92\xaf\x8d}\xbc\xd2c\x14J\x90\ +&\xcba\x97\x8e\xf9\x0a\xf9\xd5\x1aa\xeb\x0c\x1bw\xfe\ +5\xc4\x92\x8aP,\x9f\x12\xb1 \xf6\xe0\x92\xe8\x15\xd5\ +$\xa4\x09\x993\x92)\xb0\x12Z\x98c\xc7\x08@\xd6\ +i\xcc\xc0\xec\x9a*\xda\x82\x860\x9e'\xc4b\x0f\x1c\ +UH+\xb9\xb5Q9\xe5\xac\xe2[\x18-\xd2\x5c\x11\ +\x82\xb9\x80\xd4\xa6m\xba\xab:\x5c\x16\xac\xf1\xc5\x0b)\ +\xadF\x18P\xe9<\x1c\xb0\xb09\xd1)\xa1R\xc2\x9e\ +\x00fv;F\xc8\x0f\x1f\x1ez\x92P\x19\xe5;L\ +\xb8\x914\xcdU(\x07\xfe\x02\xef]\x8a\xd0\xb5\xbd\xcc\ +\x9c\xeeU\xb9|\xe6-\x85\xab\x0aiw\xd1[q\x16\ +\xb0\x9e%}\xc0\xf1U\xfd\xc9\x96\x9cM5\xc8s\xf4\ +\x9dw\x96]\xb7E\xd5Y\xe2\xed\x94Q\xad\xf0l\x0f\ +<\xb0\xe2|\xb0\xd5\xe2MmD\xad\xe8\x0c\x80/\xb7\ +\x80E\xe5\xb9V\x85uUf\x1b\x09ey\xceQ\x02\ +\xa8\xe2P;O\xeeaxQ\xac\xffpI~\xda\xe7\ +\x19\x88\xb8\x12\x04\xe0)w\x08\x1e\xb7\xed\x9d\xad\x1c\x02\ +s\xd4\x0f\x1e\xd4\xfc\xe2]\x93\xe2j\x04\xc6\xbe\xb5\x11\ +\xc5i\x1c\x98\x89\x05_\x22\x949\xdb\xcfoz\xe0\xa3\ +\xb5\x1f\xcdK+N\xb2\x05\xbbM\xcfyW\xe1\xfc\x8c\ +\xfd\x17\xcc\xa1\x83s\xcbh#\x04(\xbf\x9b\x87\x93V\ +@p/\xe9\xf21\xcco\x0b\xe4.l&\xe2l\x9d\ +8O\x8c\xab\xfa\x94\x83\xd5\x09V\x0e\x94k\x1d\xbe\x13\ +\xe8\xda\xa0\xfdg\xed\x08\xc8g\xcf\xb1\xd5N\xc9P\x02\ +S\x87\xc9\xbdk\xe3\xb4\xc5\xe4\x8e\x13B+Y'@\ +-\x5c\xdd\x8a\xbeu\xae`[qzPl\x98\xfa\x9d\ +\x15\x17|y\x85,\x80H\xb3p2\xdeX\x13\xdb\x96\ +\x07P\x0a\x08N\xa2Q]\xd7\x9f\xe2Q-l],\ +\xc6\xdc\xb2\xe8\xdc\x84\xb7\xfe\x18/\x0f_K\x94\xf9\xf5\ +'?86\x97b\xb6\xf5\xd16\xf79\xdf\x0e\xc7\xc5\ +\xed\xc1w\xbb\xeb\x19\xaek(b!\xcfa\x9b\xae\x1d\ +\xb2\xc7{\x80b\xcdJ\x0f\x9eO\x03\xa3\x03\x0e\xaeN\ +\xf3\xef\x0c\xa2\x01\x06\xc1\xba\x89Z\xf85\x83\xd4J\x81\ +\xcb\x22j\xa3\xf0h\x7f\x99A\x0c\x11Cl\xe8D-\ +\xc0\xef\x99\xf1\x05tJa]5\x0bZ\xb0\xe5\x15\xa2\ +\xaf\xc37q\x06R\xfd\xc2\x93\xcf'\xcf\xf1\xb2\xd1\x9b\ +Lq\x91\xe0\x13\xffp\x9e\xdf>R<\x1c~\x0bx\ +\x12\xc6\x19p3j\x9a\x0a\xb4i\xed\xa5\xb3\x06\xabc\ +\x07\xd0L\x8b\xd9VZN\xb8\xc2\xdcy\x16\x0a[\xed\ +h\x160:7h\x0f\xee\xc0)\x5c\xde\xc7]\x95\xf6\ +1\xc0\x13\xf4\xa2\x7f\x16\x03&\xd9}\x08\x07\xa7\xc6M\ +\xee\xccB\xee\xf1\xcc9\x86\x87\x0bv\x0bK\xae\xcc\xc9\ +\xadW\xfcs\xeb7\xabz\x0f^\x0a\xc2WP\x0e\xe7\ +\x90\xe2\x0a&+|\xeel\x85u\xefN\xeb\x83w\x11\ +\x95A\xfd\x9b\xdfB\x99z\x1e\x19\x85U\xdc\xae\xdb\x5c\ +\xd5\x16R\xc5\x9az8\xed\x1caH\x1980B`\ +\xf3>'\x97\x89\xb8qO\xb4\xda\xe6\x92\xd1K?\xf7\ +%3\x0d\xb5{\x183\xa8\x83\x7f\x1a'M\xfd{\x18\ ++\xa7\xce\xd0\xff\x81\xbc\xb8\xad86\xa0`\xec\xd0;\ +p\x90\xa0n[.\xd15\xf8\xb7\xcb\xfd\xf7,j\xaf\ +\x1d\xbb\xd4\x5c\xf2i_*\xee\xcc\x0a_\x1b\xb3\x0b^\ +(\x89THS\xd76-\xed]\xfd\xea\x83m\x0f\xbf\ +\xeft\xcf\x14zq3p\xa6\xc6K\xb8\xca\ +`\x81\xc6 \x7f\x10\xdc\x95\x19U\xeac\x0e\xd0\x8f\x87\ +y\x87l\x86\xf2Lb*W\xfc&\x19\x99\xfc \xc5\ +\x5c-\xa8`\xba0\xferi\xb0\xfb=r\xa09\x09\ +[\xa2q\x22\xbf\x10\xf4\xf5B\x90#D\x06?\xed\xd4\ +\x5c\xc5\xfd\xa0m|\x1d=W\x0a\x8f\xdb\xa8p\x02;\ +\xd2\xc0\xf7q\x82\xd4\x15\xe64u\xf3\xdcq\xb7vN\ ++:kj\x8d\xbf\xd7\xed\xf7\x9dNL\xd0\x1d\xe7\x1e\ +\xf3\xb1\xaa\xdb\xfe\xe7A\x80\x81zz\x03[\xc0\xaa\xa9\ +\xb4\xf1\xa6\xc1\x82\x84\x86h\x070\xac\xfa\xac9f\x0d\ +w\xfa@\xf6\xd3\xe1\xd9f\x8d.\xb4j_\xed\xed\x0b\ +\xae\x8c^\x0a\x8e\x16\xd8\x1e\x17\xe4~\x13\x0a\x0d\x14\xa4\ +\x0b\xab*I\xaduB\xab\x84\x0aD\x88O\xa4\x87\xd7\ +\x103\xc5C eI,M\xb3pb\x89\xc1\xe3'\ +\xf2\x0d\xd8:/\x81YPl)\xe6\x81\x0dfA\x5c\ +JN\xa1\xe7I-2Ca\xf4KHN\x1cn]\ +\xf9G&\xd7@\x9f\x01\xf7ll3\x94\xa0}\xb1\x9a\ +\xeeK\x92\xbaR(\x22\xbdT\xe8\xee>\x84U\x5c\xaf\ +\xe8\xd5\xed\xf5\xb0\xa1\xf9\xb7S\xfe\xffL\x9b\x7f\xc1o\ +o;\xf1\xb3\xe7\xec\xe5\x7f9\x1fC\xa3_\xc4\x97s\ +{s\xee\xcby\x8c\xfe\x02O\xe4\x17\xf2\ +\x00\x00\x03H\ /\ / Copyright (C) \ 2017 The Qt Comp\ @@ -677,20 +818,36 @@ Wrap\x0a \ horizontalAlign\ ment: Qt.AlignHC\ enter\x0a \ - text: \x22Tumbler\ - is used to sele\ -ct a value by sp\ -inning a wheel.\x22\ -\x0a }\x0a\x0a \ - Tumbler {\x0a \ - model:\ - 10\x0a \ + text: qsTr(\x22Sl\ +ider is used to \ +select a value b\ +y sliding a hand\ +le along a track\ +.\x22)\x0a }\x0a\x0a \ + Slider {\x0a\ + enab\ +led: !GalleryCon\ +fig.disabled\x0a \ + value: \ +0.5\x0a \ anchors.horizont\ alCenter: parent\ .horizontalCente\ -r\x0a }\x0a \ -}\x0a}\x0a\ -\x00\x00\x04i\ +r\x0a }\x0a\x0a \ + Slider {\x0a \ + enable\ +d: !GalleryConfi\ +g.disabled\x0a \ + orientati\ +on: Qt.Vertical\x0a\ + valu\ +e: 0.5\x0a \ + anchors.horiz\ +ontalCenter: par\ +ent.horizontalCe\ +nter\x0a }\x0a \ + }\x0a}\x0a\ +\x00\x00\x02x\ /\ / Copyright (C) \ 2017 The Qt Comp\ @@ -700,70 +857,618 @@ ier: LicenseRef-\ Qt-Commercial OR\ BSD-3-Clause\x0a\x0ai\ mport QtQuick\x0aim\ -port QtQuick.Lay\ -outs\x0aimport QtQu\ -ick.Controls\x0a\x0aSc\ -rollablePage {\x0a \ - id: page\x0a\x0a \ - Column {\x0a \ - spacing: 40\x0a \ - width: par\ -ent.width\x0a\x0a \ - Label {\x0a \ - width: pa\ -rent.width\x0a \ - wrapMode:\ - Label.Wrap\x0a \ - horizont\ -alAlignment: Qt.\ -AlignHCenter\x0a \ - text: \x22\ -Button presents \ -a push-button th\ -at can be pushed\ - or clicked by t\ -he user. \x22\x0a \ - + \x22Bu\ -ttons are normal\ -ly used to perfo\ -rm an action, or\ - to answer a que\ -stion.\x22\x0a \ -}\x0a\x0a Colum\ -nLayout {\x0a \ - spacing: 2\ -0\x0a an\ -chors.horizontal\ -Center: parent.h\ -orizontalCenter\x0a\ -\x0a But\ -ton {\x0a \ - text: \x22Fir\ -st\x22\x0a \ - Layout.fillW\ -idth: true\x0a \ - }\x0a \ - Button {\x0a \ - id\ -: button\x0a \ - text: \x22\ -Second\x22\x0a \ - highligh\ -ted: true\x0a \ +port QtQuick.Con\ +trols\x0a\x0aScrollabl\ +ePage {\x0a id: \ +page\x0a\x0a Column\ + {\x0a spaci\ +ng: 40\x0a w\ +idth: parent.wid\ +th\x0a\x0a Labe\ +l {\x0a \ +width: parent.wi\ +dth\x0a \ +wrapMode: Label.\ +Wrap\x0a \ + horizontalAlign\ +ment: Qt.AlignHC\ +enter\x0a \ + text: qsTr(\x22Tu\ +mbler is used to\ + select a value \ +by spinning a wh\ +eel.\x22)\x0a }\ +\x0a\x0a Tumble\ +r {\x0a \ +enabled: !Galler\ +yConfig.disabled\ +\x0a mod\ +el: 10\x0a \ + anchors.horiz\ +ontalCenter: par\ +ent.horizontalCe\ +nter\x0a }\x0a \ + }\x0a}\x0a\ +\x00\x00\x0b\xb4\ +/\ +/ Copyright (C) \ +2025 The Qt Comp\ +any Ltd.\x0a// SPDX\ +-License-Identif\ +ier: LicenseRef-\ +Qt-Commercial OR\ + BSD-3-Clause\x0a\x0ai\ +mport QtQuick\x0aim\ +port QtQuick.Con\ +trols\x0aimport QtQ\ +uick.Layouts\x0aimp\ +ort Qt.labs.qmlm\ +odels\x0a\x0aPage {\x0a \ + id: page\x0a\x0a \ +GridLayout {\x0a \ + anchors.fil\ +l: parent\x0a \ + anchors.margin\ +s: 10\x0a\x0a L\ +abel {\x0a \ + wrapMode: Lab\ +el.Wrap\x0a \ + horizontalAl\ +ignment: Qt.Alig\ +nHCenter\x0a \ + text: qsTr(\ +\x22TreeView provid\ +es a hierarchica\ +l view for displ\ +aying and \x22\x0a \ + \ + + \x22navigating t\ +ree-structured d\ +ata, allowing us\ +ers to expand an\ +d \x22\x0a \ + + \x22coll\ +apse nodes to ex\ +plore parent-chi\ +ld relationships\ + within a model\x22\ +)\x0a\x0a L\ +ayout.fillWidth:\ + true\x0a \ + Layout.columnS\ +pan: 2\x0a }\ +\x0a\x0a Item {\ +\x0a imp\ +licitHeight: 40\x0a\ +\x0a Lay\ +out.columnSpan: \ +2\x0a La\ +yout.row: 1\x0a \ + }\x0a\x0a H\ +orizontalHeaderV\ +iew {\x0a \ + clip: true\x0a \ + enabled\ +: !GalleryConfig\ +.disabled\x0a \ + syncView: \ +treeView\x0a \ + model: [qsT\ +r(\x22Location\x22)]\x0a\x0a\ + Layo\ +ut.column: 1\x0a \ + Layout.\ +row: 2\x0a \ + Layout.fillWi\ +dth: true\x0a \ + }\x0a\x0a Ver\ +ticalHeaderView \ +{\x0a cl\ +ip: true\x0a \ + enabled: !G\ +alleryConfig.dis\ +abled\x0a \ + syncView: tree\ +View\x0a \ + model: Array.fr\ +om({length: tree\ +View.rows}, (v, \ +k) => k + 1)\x0a\x0a \ Layout\ -.fillWidth: true\ -\x0a }\x0a \ - Butto\ -n {\x0a \ - text: \x22Third\ -\x22\x0a \ - enabled: false\ +.column: 0\x0a \ + Layout.ro\ +w: 3\x0a \ + Layout.fillHeig\ +ht: true\x0a \ + }\x0a\x0a Tree\ +View {\x0a \ + id: treeView\x0a\ + clip\ +: true\x0a \ + enabled: !Gal\ +leryConfig.disab\ +led\x0a \ +rowSpacing: 2\x0a \ + model:\ + treeModel\x0a\x0a \ + Layout.c\ +olumn: 1\x0a \ + Layout.row:\ + 3\x0a L\ +ayout.fillWidth:\ + true\x0a \ + Layout.fillHei\ +ght: true\x0a\x0a \ + selection\ +Model: ItemSelec\ +tionModel {}\x0a \ + delegat\ +e: TreeViewDeleg\ +ate { }\x0a\x0a \ + columnWidth\ +Provider: (colum\ +n) => column ===\ + 0 ? treeView.wi\ +dth : 0\x0a\x0a \ + Component.o\ +nCompleted: expa\ +ndRecursively()\x0a\ + }\x0a }\x0a\ +\x0a TreeModel {\ +\x0a id: tre\ +eModel\x0a\x0a \ +TableModelColumn\ + { display: \x22loc\ +ation\x22 }\x0a\x0a \ + rows: [\x0a \ + {\x0a \ + location\ +: qsTr(\x22America\x22\ +),\x0a \ + rows: [\x0a \ + {\ + location: qsTr(\ +\x22Brazil\x22) },\x0a \ + \ + {\x0a \ + locat\ +ion: qsTr(\x22Canad\ +a\x22),\x0a \ + row\ +s: [\x0a \ + \ + { location: qsT\ +r(\x22Calgary\x22) },\x0a\ + \ + { lo\ +cation: qsTr(\x22Va\ +ncouver\x22) }\x0a \ + \ + ]\x0a \ + }\x0a \ + ]\x0a \ + },\x0a \ + { locat\ +ion: qsTr(\x22Asia\x22\ +) },\x0a \ + {\x0a \ + location: qsT\ +r(\x22Europe\x22),\x0a \ + row\ +s: [\x0a \ + {\x0a \ + \ + location: qsT\ +r(\x22Italy\x22),\x0a \ + \ + rows: [\x0a \ + \ + { locati\ +on: qsTr(\x22Milan\x22\ +) },\x0a \ + \ + { location: qsT\ +r(\x22Rome\x22) }\x0a \ + \ + ]\x0a \ + },\x0a \ + \ + { location: qsT\ +r(\x22Portugal\x22) }\x0a\ + \ +]\x0a }\x0a\ +\x0a ]\x0a }\ +\x0a}\x0a\ +\x00\x00\x03&\ +\x00\ +\x00\x0c\xb5x\x9c\xd5VKo\xdb0\x0c\xbe\xfbWp\ +\x01\x0a$H\xed<\xb6]\x8c\xb6\xc0\x9a`]\x81\xf4\ +\xdd\xa1\xdb\xa9Pm:\x11*K\x9e,\xa3\xf3\x86\xfc\ +\xf7Iv\x9a\xf8\x99\xa5+v(O\x12\xc9\x8f\x22)\ +\x92\xd2`\x00\x13\x11\xa5\x92\xce\x17\x0a\xba\x93\x1e\x8c\x87\ +\xe3\x8fp\xbb@\xb8RZ\x12F\x84\xa70S\xbec\ +\x0d\x06ps9\xfdf\xcf\xa8\x87/\x9d\xc3\xfc\ +\xd6M\x8199\xc7\x86Q+\x9a\x06\xd0\xdd`\x0f`\ +\xd8\xdbr\x94\xa1\xaa\xf1C\x18\xb5[7d\xdb\x1b\x88\ +)\xc4V\xe5% \x8b\xf1\xc5\xc7\xaf\xbdo7\xdc(\ +\xa9s\xeb\x9cS\x85a\x8bC\x85\x12\xfc\x92U\xce\x96\ +\xbbn)\xd7f\xd5\xfa\xb8*\xd2s\xc3yY7\x9d\ +\xf2\xe7Fk\x05\x14\xeblo\x04{\xe3N\xcf!r\ +\xde\xdd$r5\x132\xc69\x09\xb1[\xc9q\xaf\xb9\ +8[\xa8b\xdc\xdcx3~\x97\xfc\xbf\x91\xfe=\xfa\ +\xbf\xfd\xdb\xdf\xb9\x7f\x8ft7\x8e_\xde\xc1\xf5\x01[\ +\xa4~\xffm6\xf0\xb2\xfc\x1e\x983[\x1f\x1b/{\ +\xfab\xfd\xbe\xed\xfa\xd6lQ+\xcd\x83\x9a\xe2\x94\xa4\ +\x17\xc1\x1d\xe2\xa3~\xfcZr\x95w\xa4\x0b\xd5\x1e}\ +\xfdp\xc9T\xf3`\xdd\x86\xaaZ\xd6\xdd5\x9e\x9e'\ +\xe1\x03\xca\xda_\xec\x9f]6UTTm\xad\xaaL\ +\xc5\xad\xd6\xcc\xab\xe6qC\x88\x9b\x0fY\xcb\xa0\xf1\x0b\ +.l\x8d^\x7f\x01\xf3e\xb7\x83\xfc\xfe\xebM\xcb\x5c\ +\xc8\xe3\xf7\x12i&\xf7T\x7f\xdc\x9c9\xaa\xcf\x09c\ +\xdf\xb5\xa0\xdb\x8cY\xa5\xa2\x02\xca\x5coAH$\xbe\ +\xe0,\xd5\xdfL\x11\xa1T\xa9\xfeH*,ZpM\ +\xef\x81Y\xb5\x98\xd8\xb1\xb6\xaa}W^-\xad\xa5\xf5\ +\x07\x08T\x18\x9d\ +\x00\x00\x07\xc4\ +/\ +/ Copyright (C) \ +2025 The Qt Comp\ +any Ltd.\x0a// SPDX\ +-License-Identif\ +ier: LicenseRef-\ +Qt-Commercial OR\ + BSD-3-Clause\x0a\x0ai\ +mport QtQuick\x0aim\ +port QtQuick.Con\ +trols\x0aimport QtQ\ +uick.Layouts\x0a\x0aPa\ +ge {\x0a id: pag\ +e\x0a enabled: !\ +GalleryConfig.di\ +sabled\x0a\x0a Colu\ +mnLayout {\x0a \ + anchors.fill:\ + parent\x0a \ +spacing: 40\x0a\x0a \ + CheckBox {\x0a\ + id: \ +orientationCheck\ +Box\x0a \ +text: qsTr(\x22Vert\ +ical\x22)\x0a }\ +\x0a\x0a Label \ +{\x0a wr\ +apMode: Label.Wr\ +ap\x0a h\ +orizontalAlignme\ +nt: Qt.AlignHCen\ +ter\x0a \ +text: qsTr(\x22Spli\ +tView provides a\ + container that \ +arranges items h\ +orizontally \x22\x0a \ + \ + + \x22or vertica\ +lly, separated b\ +y draggable spli\ +tters, allowing \ +users \x22\x0a \ + + \x22\ +to interactively\ + resize adjacent\ + views within an\ + application.\x22)\x0a\ + Layo\ +ut.fillWidth: tr\ +ue\x0a }\x0a\x0a \ + SplitView \ +{\x0a or\ +ientation: orien\ +tationCheckBox.c\ +hecked ? Qt.Vert\ +ical : Qt.Horizo\ +ntal\x0a \ + Layout.fillHeig\ +ht: true\x0a \ + Layout.fill\ +Width: true\x0a\x0a \ + Rectang\ +le {\x0a \ + implicitWid\ +th: 200\x0a \ + implicit\ +Height: 100\x0a \ + colo\ +r: \x22lightblue\x22\x0a \ + S\ +plitView.maximum\ +Width: 400\x0a\x0a \ + Labe\ +l {\x0a \ + text: \x22V\ +iew 1\x22\x0a \ + ancho\ +rs.centerIn: par\ +ent\x0a \ + }\x0a \ + }\x0a\x0a \ + Rectangle {\x0a \ + id:\ + centerItem\x0a \ + colo\ +r: \x22lightgray\x22\x0a \ + S\ +plitView.minimum\ +Width: 50\x0a \ + SplitV\ +iew.minimumHeigh\ +t: 50\x0a \ + SplitView.\ +fillWidth: true\x0a\ + \ +SplitView.fillHe\ +ight: true\x0a\x0a \ + Labe\ +l {\x0a \ + text: \x22V\ +iew 2\x22\x0a \ + ancho\ +rs.centerIn: par\ +ent\x0a \ + }\x0a \ + }\x0a\x0a \ + Rectangle {\x0a \ + imp\ +licitWidth: 200\x0a\ + \ +implicitHeight: \ +100\x0a \ + color: \x22ligh\ +tgreen\x22\x0a\x0a \ + Label {\ \x0a \ + text: \x22View\ + 3\x22\x0a \ + anchors.\ +centerIn: parent\ +\x0a \ + }\x0a }\ +\x0a }\x0a }\ +\x0a}\x0a\ +\x00\x00\x09\x93\ +/\ +/ Copyright (C) \ +2025 The Qt Comp\ +any Ltd.\x0a// SPDX\ +-License-Identif\ +ier: LicenseRef-\ +Qt-Commercial OR\ + BSD-3-Clause\x0a\x0ai\ +mport QtQuick\x0aim\ +port QtQuick.Con\ +trols\x0aimport QtQ\ +uick.Layouts\x0aimp\ +ort Qt.labs.qmlm\ +odels\x0a\x0aPage {\x0a \ + id: page\x0a e\ +nabled: !Gallery\ +Config.disabled\x0a\ +\x0a GridLayout \ +{\x0a anchor\ +s.fill: parent\x0a\x0a\ + Label {\x0a\ + wrap\ +Mode: Label.Wrap\ +\x0a hor\ +izontalAlignment\ +: Qt.AlignHCente\ +r\x0a te\ +xt: qsTr(\x22TableV\ +iew provides a s\ +crollable grid t\ +hat displays dat\ +a from \x22\x0a \ + \ ++ \x22a model in ro\ +ws and columns, \ +allowing users t\ +o view and inter\ +act \x22\x0a \ + + \x22\ +with structured \ +information with\ +in an applicatio\ +n.\x22)\x0a \ Layout.fillWidt\ h: true\x0a \ - }\x0a }\x0a\ - }\x0a}\x0a\ -\x00\x00\x03\xa5\ + Layout.colum\ +nSpan: 2\x0a \ + }\x0a\x0a Hori\ +zontalHeaderView\ + {\x0a c\ +lip: true\x0a \ + syncView: \ +tableView\x0a \ + model: tab\ +leModel.headerMo\ +del\x0a \ +Layout.column: 1\ +\x0a Lay\ +out.row: 1\x0a \ + Layout.fi\ +llWidth: true\x0a \ + }\x0a\x0a \ + VerticalHeaderV\ +iew {\x0a \ + clip: true\x0a \ + syncVie\ +w: tableView\x0a \ + Layout.\ +column: 0\x0a \ + Layout.row\ +: 2\x0a \ +Layout.fillHeigh\ +t: true\x0a \ +}\x0a\x0a Table\ +View {\x0a \ + id: tableView\ +\x0a col\ +umnSpacing: 1\x0a \ + rowSpa\ +cing: 1\x0a \ + clip: true\x0a\x0a\ + sele\ +ctionModel: Item\ +SelectionModel {\ +}\x0a mo\ +del: tableModel\x0a\ +\x0a Lay\ +out.column: 1\x0a \ + Layout\ +.row: 2\x0a \ + Layout.fillW\ +idth: true\x0a \ + Layout.fi\ +llHeight: true\x0a\x0a\ + dele\ +gate: TableViewD\ +elegate {\x0a \ + implic\ +itWidth: 100\x0a \ + imp\ +licitHeight: 50\x0a\ + \ +Component.onComp\ +leted: {\x0a \ + if \ +(contentItem as \ +Label) {\x0a \ + \ + contentItem.hor\ +izontalAlignment\ + = Qt.AlignHCent\ +er\x0a \ + conte\ +ntItem.verticalA\ +lignment = Qt.Al\ +ignVCenter\x0a \ + }\ +\x0a \ + }\x0a }\ +\x0a }\x0a }\ +\x0a\x0a TableModel\ + {\x0a id: t\ +ableModel\x0a \ + property var h\ +eaderModel: [qsT\ +r(\x22Name\x22), qsTr(\ +\x22Color\x22)]\x0a \ + TableModelColu\ +mn { display: \x22n\ +ame\x22 }\x0a T\ +ableModelColumn \ +{ display: \x22colo\ +r\x22 }\x0a row\ +s: [\x0a \ + {\x0a \ + \x22name\x22: qsTr(\ +\x22cat\x22),\x0a \ + \x22color\x22:\ + qsTr(\x22black\x22)\x0a \ + },\x0a \ + {\x0a \ + \x22nam\ +e\x22: qsTr(\x22dog\x22),\ +\x0a \ + \x22color\x22: qsTr(\x22\ +brown\x22)\x0a \ + },\x0a \ + {\x0a \ + \x22name\x22: qsT\ +r(\x22bird\x22),\x0a \ + \x22colo\ +r\x22: qsTr(\x22white\x22\ +)\x0a }\x0a\ + ]\x0a }\x0a\ +}\x0a\ +\x00\x00\x02D\ +\x00\ +\x00\x08Ux\x9c\xd5TMo\x9b@\x10\xbd\xf3+\xa6\ +>%j\xc1nZ\xa9\x12\x97\xaa&jS\xc9U\xfd\ +\x11)\xbd\xaea0\xab,\xbbdw\x91C\xab\xfc\xf7\ +\xce\x02v\x0c8Q\xd3(R\xcb\xc5\x9e\x997o\x87\ +\xb7\xf3\x18\x8f!RE\xa5\xf9&\xb3p\x12\x9d\xc2\xd9\ +\xe4\xed\x07\xb8\xcc\x10\x16\x96*y\xc1d\x053\x9b\x04\ +\xdex\x0c\xab\xf9\xf9\x0f\x7f\xc6c\x94\x06\xfd\xaf\x09J\ +\xcbS\x8e:\x846\xb7\xc4\xd4_X\x9f\xdar\xd41\ +g\x02\xbe/a\xba:\xf7\xdf\xf9\x91`\xa5A\xcf\xe3\ +y\xa1\xb4%\xf2E\xc9\xe3\xeb^\x18\xccX\xa5Jk\ +\xfa\xe9HI\xab\x950\x9e\xb7\x8a\xe9W\xb0\xb5\xc09\ +\xdb \xfc\xf2\x80\x1e\x9e\x84PP\xe8\xd5Q\xa4D\x99\ +\xcb\xb6\xe4\x1eS\xb0\x98\xcbM\x08\xef'\xfb\xdc\x96'\ +6s]\x9a^\x22\xa8#o_\x5c\xaa\xedA{\xcd\ +\x99a|=U\xb7\xbd\xf4\xee\xf0\xd8\x951\xd9\xa1\x06\ +\x18\x8b\xb76\x84\x1bs\xa9OFQ\x83\x1d\x9dvP\ +w\xdeS\xceK\x05\xb3\x7ft\xd8g\x02>\xeb\xa4B\ +\xa31\x8f\xbc\x19Jw\x17\x04|\xf5\x85\x09\x81\xba\xa2\ +\xabJ\xf9&H\xb8\xa9\x0b\x8fN7o\xc8\x07\x03\x1e\ +\x19u\xc6\xd6(z3\x1e\xbb\xc3N]\xb3\xe2\x9bJ\ +0l\xba\x83+\x8a;\x80Li\xfe\x93V\x8b\x89O\ +\x82odN4!\xed\x5cPG\x17\x11\x85\xa8;\x0d\ +\x87\xc3OKk\x95\xac\x05\x22\xa0\x01\x06Ei2\x7f\ +\xdd\xa4m\xc6,\xc4L\xc2\x1a\xeb<&\xa04\xc4\x82\ +\xbb\xbb\x87uE\x00\x042\x84\x0e`4\xd0\xe85\xb4\ +\xec\xc4\xaa\x11\xa4\xd29\x89[9|\x02VA\x81:\ +\xa5\x1c\x10=\x8b-W\xf2\x8d#\xa7\x02\x93f\x8b\x9a\ +F\xb9)\xd1\xb8Bp\xa0\xed\x81\x9a\x8dE\x1a\xb3\xf5\ +D\xdd\x9b\xe5l\xd2\xc93\x19\x93\x5c&\xb8\xd7\xac\xd1\ +g\xaf\x7f\xbf\xd0]\xb3V\xad\xe1\x92=k\x81\x1a\xd6\ +\xde\xfe\xb8\xa75\xe4\xc0\x99A\x1b\x0f\x1a\x9c\xa3\xba\xbe\ +z\x10\x9a\xa8\xad\x1c\x18c\x87\x86\x8f`u\x89\x10B\ +)\x13L\xb9\xb8\x95\x03%\x16\x81\x08\x08\xa6\xe8\xf3\x14\ -}\x9c]\x0c\xce\x06c\x8a\xd3\x04\x1c\x87D1\x17R\ -)\xbfMIp\xbf3\xf4&x\xcdS\x99\xecN\x8f\ -9\x93\x82\xd3\xc4qn0\x03\xf4\x8f\x83\xd4o\xcci\ -\x1a\xb1\x5c\xa2\x98\xd2\xbf$\xc6\x01a\x0b\x1f\x9d\x9f\x96\ -s\x98\x05K.\x12oN(\xf5Q\x8c\x85\xday\x85\ -(y|\x8d\xc5\x820_a\xe0\x94\xe4\x09\xbe\x03j\ -\xe8\xcf\xe7\xf4\xa2\x99\xba/$\x94K\x1fI\x91\x82\xc5\ -\xb2\x128\xbe\xe6!\xf8\xb9\x02\xef\x8b\x1a[\x0cjM\ -\xf2M\xd9\x85\xe9\x07J\x16,R[\xf2\x95\xc1^6\ -\xba\x1c\xab!\x08K@\xc2\x93\xe2p/\x80\xc2\x02K\ -@A\x01\x0aR\xe6 \x85m\x88p\x82\xc2\x82\x9a \ -\xc2\xd0#\x81U\x82\x924Xj\xd2\x84$\xf2O5\ -\xe3\xb9\xa5\xdag\xc3\xcc\x82\xbac)\x09}D\x0b\x92\ -E\x08(\x89k\xccN \x90\x843/\x16<\x06!\ -\xd7j\xc3r\x1d\x83[\xcb\xb4\xd9\xac\x8f\x0c\xb7\xee\xae\ -^0k\x8e\x0a]\xc0CJ\x84\xb2|\xb3\x1cJ\xa4\ -P\xce\xdf\x08U\x04V\xb9\xb7J(4B^6Y\ -a]\x82>\x12\xe5\xfa\xb9\x17U\x5cR\x12\x10y\x99\ -\x11\xd1O:Rv\x05\xeb\x02\xa6\xc6\xa0\x8c\xad\x96)\ -w\xb4a\xb7\xd7d\x8e\x19\xbeA\x161W\xac\x12\xdf\ -[g\xdb\xa3C#\xda`\xb9,\xb0\xc9x\x1c\xdb#\ -8\xe4\x8c\xae\xb7\x1ey\xc4\xa2\x8cJ\x9dO8S\x9b\ -\xbb\xc6*x\xaa\x10\xb9W\x12\xa2M\x80\xbb>\x22\xc6\ -\xb0\x94\xfd\xb9*6[\x91\x18\x0c\xb9\xc4\x1c\xb7\x09\x8e\ -\x97\x10\xdc\x1b\x82\x819n\x13\x9c\xe2\x90pCP\x98\ -\xe3=[\x95\xc1\xd2\xde\xab1Q\x8a\xb69\xabdj\ -82\xb5\xb09\x15V\x13\xec\x86xU9\xff!\xa2\ -\x94\xa8\xa5B\x92\xe0;\xaa2\x0d{H1\xd5y?\ -l\x89\xdeGLS\xd8\xa7\x12X'\x8d\xc5\xa1\xcd\xc3\ -\xba\xe1\xb4>\xbf\x06\xaf\xfap\xa9\x02f\x85Y\xdb\x09\ -7\xf9~,\x5ck\x19\xdb\xf03\xed\x16\x10\xf1\xc76\ -\x007\xbf\xa9\xcag\x98-(\xb4(\xcc\x8a\x0c\xa7\x5c\ -u\x17\x16\xee\xaa\xb4@\xa2\xab\xdd\xaf\xc8}wvv\ -\xe6\x22Uc\xde\x9d\x9f\x9f\xbb\xad\xaa\xba\x04Q]\xfa\ -/\x04\xf2a\xfbf\xb7\x15\xb1\x95\xcf\xb6\x86\xb3\xb1\xaa\ -(\xf7\x10\xfa{\xb0xa\xd8\x98\xbf\xac\xdaE\xaa)\ -\xa1^\xee\xaa#\x9e\xaa\x8a\x11\xc2\xd3\xf1!Kw\x0c\ -\xaf\x86\xc3xH\xb9\xec\xd1\xf6\xb9\xea\x96\xbc\x98<\x01\ -\x9d\x91o\xb0sF=\x9b\xfa\x86`\x18\x8d\xdc4s\ -\x81\xbb\x97\xbd8\x04\xeej\xa9\xf2\xfa~\xf6\xce\xfd@\ -s\xfa\xdcOi\xf0\xe9L\xf5c\xfaR\xa0zZF\ -\x22\xac{\x96N)\xa3\xe4n\x0e\x95\x9b\xa2\x9f\xf8\x10\ -\xec\xd1\xfa\xcaX\x91\xea\x0a\x00\xca=\x9b\x96E\x17I\ -\xe7mb\xc1\xe8\x91\xcb\x86T-\x8b\xd7]\x22#+\ -\x045]Z7\xaf\xfe\x9eFw \xba8\xe9\x0d\xe0\ -\xf4\xc8w\xc14O\xd9\xed(J\xee\xa3\xd3V\x0e\xc0\ -\x89\xbaPx\xfa\x12\xe3\xa3O\xf9\xe0\x8a}N\xd5\xcd\ -\x14\x87/@\xfe\xff8\xae\x8d\xe39\xa6\x09\xf4\x93\x84\ -\xb2\xcc\xeeQ\x98\xcbjG\xd2\xcc/\xf2r\xdfE\xa0\ -4\x92\xb3\xdc\xc2J\x1a\xf3\x12\x05\xbf<:\xee\xb7M\ -\xad\xbf\x9cTA\xb0.5\xff\xd5\xc6\xbe\x15\x89\x8f\xa9\ -\x94\x9c\xfd&x\x1a7`\x91\xdd\xb7\x0c\xb6\xd7\xe0Z\ -\x7fw\xab\xe2j\xdd\xf9\xbe\x1f\xae\x86\x95\xdeB\xff\xdd\ -c\xfd\x1b\xf8\xa3\xc3\x05\xaa\xf6\x0e[w\x832\x18\x7f\ -\xc8\xd8\xcc\xfa\xe7\xfc\x09I\xbf\xef\xd5\xb5\xae\x9a\xf6\x89\ -B\x94\x01\x86\xf2\xd2a?m\xbc\xdf\xe4=kz\xe8\ -\xd6d\xba\x97*\x1b\xf5\xa9\xec\xac\xb32\xfb)f\xab\ -\xcd\x9a\x1f\xf6\xabn\xd4\xaf\xba\xee\xc6\xda\xcfG[u\ -\xd6\xfc\xb0_u\xa3~\xd5u7\xd6~\xf2\xda\xaa\xb3\ -\xe6\x87\xfd\xaa\x1b\xf5\xab\xee\xa006\x9f\xe9\xacH1\ -\x08\xc3\xbe\x15\x8e\xfaVX1y'\x9dm\xdf\xe0'\ -\x1c\x87 \x1a2\xfc\x86-gz\xcd\xbb\xfa\x81\xd9=\ -\xe1\xa9\x08\xb6%e\xfb5\xc2\xab{T\xfeK\x83\xf3\ -\xb7sX\xfa\xef\xfeY\xa1\xbe\xc04\xb2\xeb\xcdt\xe0\ -\xd6/\xe4YU\xe9\xc0\xabM \xfa\xc5\xa6\xba\xeb\x92\ -\xa7p\xa4\xd9\xf9\xefu\xe0\x81^\xd9\xae\xb5\xf9l\xa4\ -=\xde\xf0\xa9\xe8\xf0\x02l\x99\xbby\xa3\xf2\x0b\xc3\xeb\ -+u\xfe\xdf\xb3\xf3\xec\xfc\x0b\x01\xe6r&\ -\x00\x00\x02\xc8\ +\x00\x1a\xf5x\x9c\xd5XmO\xdc8\x10\xfe\x9e_\xe1\ +\xa6_@\xd7\x0dmA\xaa\x94Su\x82\xa5*HK\ +\xcbKu=\xe9t\x1fL2\xbbk\xe1\xb5S\xc7\x01\ +\xb6\x15\xff\xfd\xec\xbc\xe1$N\xe2\xb4\xe8\xc4\xcd\x87\xd5\ +\xda3\xf3$\xf3x\xc6\x1ego\x0f\xcdy\xb2\x15d\ +\xb5\x96hg\xbe\x8b\xde\xbe~\xf3\x0e}Y\x03\xba\x90\ +J\xb3I0\xdb\xa2\x85\x8c\x03oo\x0f]\x9d\x1f\xff\ +5[\x90\x08X\x0a\xb3\xd3\x18\x98$K\x02\x22D\xe5\ +\xdc%,g\x17r\xa6\xdc6 \x22\x82)\xfa|\x89\ +\x8e\xae\x8eg\xfb\xb39\xc5Y\x0a\x9e\x97\x08\xbc\xda\xe0\ +\x1c\x993\x05p\x04k|K\xb8\xc28\xe2\x19\x8b=\ +\x8f(\x85\x90\xea\xe9\x17\x19\x89nZ\xc3`\x81\xb7<\ +\x93i{z\xce\x99\x14\x9c\xa6\x9ew\x8e\x19\xa0\x1f\x1e\ +R2\xe74\xdb\xb0\xc2\xa3\x9c\xd2\x92&8\x22l\x15\ +\xa2\x83\xd7\xf5\x1cf\xd1\x9a\x8b4X\x12JC\x94`\ +\xa1\xde\xac\xa3\x94<9\xc3bEX\xa8H\xf2j\xf5\ +\x02_\x035\xf0\x8b9\xfd\xd0\x1c\xee+\x89\xe5:D\ +Rd\xd00\xb9\x1389\xe31\x84\x05@\xf0U\x8d\ +\x1b\x06\xea\x99\xe4\xbb\x8a\x0b\xd3CJVl\xa3^)\ +T\x01\x07\xf9\xe8d\xae\x86 \x1a\x0e\x12\xee\x95\xc5\xb7\ +\xf4\x8b\xd8\xf1\x8f\x81\xc2\x0aK@Q\xc9\x0cR1!\ +\xb5\x021\xc2)\x8aKm\x8a\x08C\xb7\x04\xeeR\x94\ +f\xd1Z\xab\x16$\x95\x7f\xaa\x99\xc0\xdf\xad\xc1\x1f\x1e\ +\x83=\xca\xa4\xe4\xec\xa3\xe0Y\xd2\x0a\x99\xc4!\x128\ +&\xdc0\xb1!T\x0f\xb0\xb8\xd3R\xd5PD\x94$\ +\x16\xfaR\x88$\xe1,H\x04O@\xc8m\x88|\xb9\ +M\xc0\xb7\x1aU\xf1\x86\xc8H\x8f\xf6\xd3Kcm\xd1\ +\xd1\x0b\xf8\x96\x11\xa1\xc8\xab\x1e\x87R)T\x12UN\ +\x1d\x87\xbbb\xd5k65\xc9A>\xd91]\x83\xae\ +\xbd\xfa\xf9E6\xa8\xfc\xa6$\x22\xf2$W\xa2\xdft\ +\xc6\xb5\x1dm\x89g\x09(7\xb3\x1a\x15\x09c\xc4\x1d\ +\xf4\x85\xa3\xa5*\x83(\xcf\xbcS\xd6\xa9\x93J\x1e\xbc\ +\xe6hje\x18&'%7\xb9M\xc3h\xa3J\x87\ +\x16\x04\xeb*\xb2\xf1\xa0u\x1f(\xe8\xc2A?\x90\xce\ +\x0e\x95$\xa7\x126Uq\xf8\xbf\xa3[L3\xa8\x8a\ +\xc6\xd4\xbd\xf1w[\x81\xfc2\xe4\xdb\xa7\x87\xdcw\x87\ +\xbc\xba#\x09\xf4a6\x94\x13Bw\x07\x9d\x10\xbc;\ +\xe8\x84\xf0\xe7k\x88n\xfa@\x1b\xca\x09\xe1\xbb\x83N\ +\x08\xdf\x1dtB\xf8\x97zg\xee\x03m('\x84\xef\ +\x0e:!|w\xd0i\xc9/\xa3\xf5@N\x19\xdai\ +\xe9\xef\x0c;\xad\x00\x9ca\xbb$\xb46\xdc\xc7\xb3o\ +\xc1q\x0c\xa2\xe7\xf4\xab\xcc\x0a\xa3_9\xcfR\x9e\x89\ +\x08\xea\x06\xef\x11\xba\x9e:\xc3\xc9\xdf:\xd4\x7f<\xf7\ +\x836\xa7\xc0\xdd\x5c\xc3;X\xdfbQ\x9c$\x0e\xb6\ +D-\x11a1\xdcw\xdf\xba\xb6\xa9\x9b\x1b\xcd\x8f\xd1\ +\xd0X\xe0q\xcc\x19\xdd6_\xc5\xc6T\xd8s\xc27\ +\xcf\x88\x10\x11cX\xfb\xbf\xb2\xbb6wX\xd5\x02\x98\ +\xe31\xe7\xe6\xfe\x14\xa2\xc8\x1c\x8f97\xab\xbb\xec\x18\ +'\xbc\xb6Y\x17\xf9{\x1b\x13\xb5\xbb\xa5#\xe9L\xd5\ +\xc6\x03\x0d\x94\x95\xd2.\x94\x16s1z\x10\xb5\x00\xc3\ +\xd7\x14\x14\xf4\x8b\x8f\x98R\x10[uqY\x92U\x10\ +\x934W\xf4:\x16\xadZ\xb3H\x03{MTR\x96\ +l\xcb\xc7^\xb3Z\xba\x9b\xd3O\xd2f\xcf&;o\ +\x8dL\x1c \xae\x83\xfb\xec)\xeeu\x1a\xa3\xd0\x0cY\ +\xc0\x86\xdf\x8eqX\xc9\xa5j\xde1[\xd1!\x1e+\ +\x898\xd5w\xee\x06\xfd\xea.\x05\xa9\xbe!\xfe\x81\xfc\ +\x97\xfb\xfb\xfb>Rg\xd1\xcb\x83\x83\x03\x7f\x14\xae$\ +\xa2\xb8\x10\x0c\xe4\x98)\xd5\xbd\xa7t*\x86\xe3/\xfe\ +x\x1d\x1c\xb5mF\xc7\xd9\x5c]\xa9ntj\x8c\xf3\ +\xa3\x85,\xd1N;\x1d\xf4\xde\xfe\xe2\xfd{\x94\xa9\x83\ +`I\x18\xc4\xbbNPZ,PA~\xf6\x04\xc5*\ +\xb7\x9f\x95\x9f5\xe3\xf0\x96\x22m\xcb\xd0E\xb1-K\ +\xae\xd6\x22!\xf7@\xaf\xc8wh\x15]\xd0\xd4:\x01\ +\x9a_%.\xf3@}7\xce\xca\x14\xf5\xef\xd6j\x1f\ +\x1eOA-\xceW\xd4\xb6t\xf7>7\xed\x00\xf9W\ +\xaa\x81\xd0\x1f\xc80=dd\x83\xf5\xb5\xda\xb9\xe0k\ +\x8f\xe1\xb5=/{\x87\xc3\xc8\x01]\x8b\xc4b\x05\x9d\ +Mn\xd4\xcd\xf8\xc6R7\x80\x0a\x02o\xcb\xf5\x1c\x05\ +(\xfb\xd7\xceM\xbf-\xc3\xcb\xf0)\xdb\x5c\x83pe\ +SK\x15\xaf\xdb\xc9Q\x89\x11n\xb1)\x8d\x07(y\ +\x88\xba\x1fh\xda\x028U\xbdiPt\xf9\x1f\x8a\xc1\ +)\xfb\x9c\xc9\x8b\x0c\xf7\x9fKZ\x86\x89\xf9\xbf\xe5\xc1\ +\x12\xd3\xf4g\x13a\xa0\xe2\xf2U\x0e(,e\xf7\xe0\ +\x1c\xf6\x11\xc5I\xe4\xeaT\x07\xcfY\x11y\xa7n\x83\ +TQ,w\xec\x1b\xdd\x935Z\xf6\xce\xdbNP\xa3\ +k\x7f\x06\x1d\xea\x93\x91`\xbfA\xd8Ih\xdc>\xfe\ +c\x12z}\x8c\xef\xe5\xc1J\xff\x0e|G7\xe5)\ +\xfbu\xeb-\xaa\xafa7\x8c\x9fi\x22\xd9G\xc5\xbf\ +\x07\xef\xc1\xfb\x17\xe7\x1f\xdf\x92\ +\x00\x00\x02\xfb\ /\ / Copyright (C) \ 2017 The Qt Comp\ @@ -1065,28 +1784,31 @@ Wrap\x0a \ horizontalAlign\ ment: Qt.AlignHC\ enter\x0a \ - text: \x22The Dia\ -l is similar to \ -a traditional di\ -al knob that is \ -found on devices\ - such as \x22\x0a \ - + \x22st\ -ereos or industr\ -ial equipment. I\ -t allows the use\ -r to specify a v\ -alue within a ra\ -nge.\x22\x0a }\x0a\ -\x0a Dial {\x0a\ - valu\ -e: 0.5\x0a \ - anchors.horiz\ -ontalCenter: par\ -ent.horizontalCe\ -nter\x0a }\x0a \ - }\x0a}\x0a\ -\x00\x00\x04\xe8\ + text: qsTr(\x22Th\ +e Dial is simila\ +r to a tradition\ +al dial knob tha\ +t is found on de\ +vices such as \x22\x0a\ + \ ++ \x22stereos or in\ +dustrial equipme\ +nt. It allows th\ +e user to specif\ +y a value within\ + a range.\x22)\x0a \ + }\x0a\x0a D\ +ial {\x0a \ + enabled: !Gall\ +eryConfig.disabl\ +ed\x0a v\ +alue: 0.5\x0a \ + anchors.ho\ +rizontalCenter: \ +parent.horizonta\ +lCenter\x0a \ +}\x0a }\x0a}\x0a\ +\x00\x00\x05-\ /\ / Copyright (C) \ 2017 The Qt Comp\ @@ -1120,54 +1842,137 @@ Label.Wrap\x0a \ horizonta\ lAlignment: Qt.A\ lignHCenter\x0a \ - text: \x22F\ -rame is used to \ -layout a logical\ - group of contro\ -ls together, wit\ -hin a visual fra\ -me.\x22\x0a }\x0a\x0a\ - Frame {\x0a\ - anch\ -ors.horizontalCe\ -nter: parent.hor\ -izontalCenter\x0a\x0a \ - Colum\ -n {\x0a \ - spacing: 20\x0a\ - \ -width: page.item\ -Width\x0a\x0a \ - RadioButt\ -on {\x0a \ - text: \x22\ -First\x22\x0a \ - check\ -ed: true\x0a \ - wid\ -th: parent.width\ -\x0a \ - }\x0a \ - RadioButton {\ -\x0a \ - id: button\x0a\ + text: qs\ +Tr(\x22Frame is use\ +d to layout a lo\ +gical group of c\ +ontrols together\ +, within a visua\ +l frame.\x22)\x0a \ + }\x0a\x0a Fr\ +ame {\x0a \ + enabled: !Gall\ +eryConfig.disabl\ +ed\x0a a\ +nchors.horizonta\ +lCenter: parent.\ +horizontalCenter\ +\x0a\x0a Co\ +lumn {\x0a \ + spacing: \ +20\x0a \ + width: page.i\ +temWidth\x0a\x0a \ + RadioB\ +utton {\x0a \ + text\ +: qsTr(\x22First\x22)\x0a\ \ - text: \x22Secon\ -d\x22\x0a \ - width: pa\ -rent.width\x0a \ - }\x0a \ - Rad\ -ioButton {\x0a \ - t\ -ext: \x22Third\x22\x0a \ + checked: tru\ +e\x0a \ + width: par\ +ent.width\x0a \ + }\x0a \ + Radi\ +oButton {\x0a \ + id\ +: button\x0a \ + tex\ +t: qsTr(\x22Second\x22\ +)\x0a \ + width: par\ +ent.width\x0a \ + }\x0a \ + Radi\ +oButton {\x0a \ + te\ +xt: qsTr(\x22Third\x22\ +)\x0a \ + width: par\ +ent.width\x0a \ + }\x0a \ + }\x0a \ + }\x0a }\x0a}\x0a\ +\x00\x00\x04\xc1\ +/\ +/ Copyright (C) \ +2025 The Qt Comp\ +any Ltd.\x0a// SPDX\ +-License-Identif\ +ier: LicenseRef-\ +Qt-Commercial OR\ + BSD-3-Clause\x0a\x0ai\ +mport QtQuick\x0aim\ +port QtQuick.Con\ +trols\x0a\x0aPage {\x0a \ + id: page\x0a e\ +nabled: !Gallery\ +Config.disabled\x0a\ +\x0a header: Men\ +uBar {\x0a M\ +enu {\x0a \ + title: qsTr(\x22&\ +File\x22)\x0a \ + Action { text\ +: qsTr(\x22&New...\x22\ +) }\x0a \ +Action { text: q\ +sTr(\x22&Open...\x22) \ +}\x0a Ac\ +tion { text: qsT\ +r(\x22&Save\x22) }\x0a \ + Action \ +{ text: qsTr(\x22Sa\ +ve &As...\x22) }\x0a \ + MenuSe\ +parator { }\x0a \ + Action {\ + text: qsTr(\x22&Qu\ +it\x22) }\x0a }\ +\x0a Menu {\x0a\ + titl\ +e: qsTr(\x22&Edit\x22)\ +\x0a Act\ +ion { text: qsTr\ +(\x22Cu&t\x22) }\x0a \ + Action { \ +text: qsTr(\x22&Cop\ +y\x22) }\x0a \ + Action { text:\ + qsTr(\x22&Paste\x22) \ +}\x0a }\x0a \ + Menu {\x0a \ + title: qs\ +Tr(\x22&Help\x22)\x0a \ + Action {\ + text: qsTr(\x22&Ab\ +out\x22) }\x0a \ +}\x0a }\x0a\x0a Lab\ +el {\x0a anc\ +hors.verticalCen\ +ter: parent.vert\ +icalCenter\x0a \ + width: parent\ +.width\x0a w\ +rapMode: Label.W\ +rap\x0a hori\ +zontalAlignment:\ + Qt.AlignHCenter\ +\x0a text: q\ +sTr(\x22MenuBar pro\ +vides a horizont\ +al bar with drop\ +-down menus, \x22\x0a \ \ - width: parent.w\ -idth\x0a \ - }\x0a \ - }\x0a }\x0a \ - }\x0a}\x0a\ -\x00\x00\x05B\ ++ \x22allowing user\ +s to access grou\ +ped commands and\ + actions \x22\x0a \ + + \x22w\ +ithin an applica\ +tion.\x22)\x0a }\x0a}\x0a\ +\ +\x00\x00\x05q\ /\ / Copyright (C) \ 2017 The Qt Comp\ @@ -1185,76 +1990,79 @@ SwipeView {\x0a \ currentInde\ x: 1\x0a anc\ hors.fill: paren\ -t\x0a\x0a Repea\ -ter {\x0a \ - model: 3\x0a\x0a \ - Pane {\x0a \ - w\ -idth: SwipeView.\ -view.width\x0a \ - heigh\ -t: SwipeView.vie\ -w.height\x0a\x0a \ - Column\ - {\x0a \ - spacing: \ -40\x0a \ - width: pa\ -rent.width\x0a\x0a \ +t\x0a enable\ +d: !GalleryConfi\ +g.disabled\x0a\x0a \ + Repeater {\x0a \ + model\ +: 3\x0a\x0a \ + Pane {\x0a \ + width: S\ +wipeView.view.wi\ +dth\x0a \ + height: Swip\ +eView.view.heigh\ +t\x0a\x0a \ + Column {\x0a \ \ -Label {\x0a \ +spacing: 40\x0a \ \ width: parent.wi\ -dth\x0a \ - wrap\ -Mode: Label.Wrap\ +dth\x0a\x0a \ + Label {\ \x0a \ - horizon\ -talAlignment: Qt\ -.AlignHCenter\x0a \ + width: \ +parent.width\x0a \ \ - text: \x22Swi\ -peView provides \ -a navigation mod\ -el that simplifi\ -es horizontal pa\ -ged scrolling. \x22\ -\x0a \ - + \x22The \ -page indicator o\ -n the bottom sho\ -ws which is the \ -presently active\ - page.\x22\x0a \ - }\x0a\x0a \ + wrapMode: L\ +abel.Wrap\x0a \ \ - Image {\x0a \ + horizontalAlig\ +nment: Qt.AlignH\ +Center\x0a \ + t\ +ext: qsTr(\x22Swipe\ +View provides a \ +navigation model\ + that simplifies\ + horizontal page\ +d scrolling. \x22\x0a \ \ - source: \x22../i\ -mages/arrows.png\ -\x22\x0a \ - anchor\ -s.horizontalCent\ -er: parent.horiz\ -ontalCenter\x0a \ + + \x22The pa\ +ge indicator on \ +the bottom shows\ + which is the pr\ +esently active p\ +age.\x22)\x0a \ + }\x0a\x0a \ \ -}\x0a \ - }\x0a \ -}\x0a }\x0a \ -}\x0a\x0a PageIndic\ -ator {\x0a c\ -ount: view.count\ -\x0a current\ -Index: view.curr\ -entIndex\x0a \ - anchors.bottom:\ - parent.bottom\x0a \ - anchors.h\ -orizontalCenter:\ - parent.horizont\ -alCenter\x0a }\x0a}\ -\x0a\ -\x00\x00\x04;\ + Image {\x0a \ + \ + source: \x22../im\ +ages/arrows.png\x22\ +\x0a \ + anchors\ +.horizontalCente\ +r: parent.horizo\ +ntalCenter\x0a \ + }\ +\x0a \ + }\x0a }\ +\x0a }\x0a }\ +\x0a\x0a PageIndica\ +tor {\x0a co\ +unt: view.count\x0a\ + currentI\ +ndex: view.curre\ +ntIndex\x0a \ +anchors.bottom: \ +parent.bottom\x0a \ + anchors.ho\ +rizontalCenter: \ +parent.horizonta\ +lCenter\x0a }\x0a}\x0a\ +\ +\x00\x00\x05S\ /\ / Copyright (C) \ 2017 The Qt Comp\ @@ -1267,32 +2075,44 @@ mport QtQuick\x0aim\ port QtQuick.Con\ trols\x0a\x0aFlickable\ {\x0a id: flick\ -able\x0a\x0a conten\ -tHeight: pane.he\ -ight\x0a\x0a Pane {\ -\x0a id: pan\ -e\x0a width:\ - flickable.width\ -\x0a height:\ - flickable.heigh\ -t * 1.25\x0a\x0a \ - Column {\x0a \ - id: colum\ -n\x0a sp\ -acing: 40\x0a \ - width: par\ -ent.width\x0a\x0a \ - Label {\x0a \ - w\ -idth: parent.wid\ -th\x0a \ - wrapMode: Lab\ -el.Wrap\x0a \ - horizont\ -alAlignment: Qt.\ -AlignHCenter\x0a \ - tex\ -t: \x22ScrollBar is\ +able\x0a enabled\ +: !GalleryConfig\ +.disabled\x0a co\ +ntentHeight: pan\ +e.height\x0a\x0a Pa\ +ne {\x0a id:\ + pane\x0a wi\ +dth: flickable.w\ +idth\x0a hei\ +ght: flickable.h\ +eight * 1.25\x0a\x0a \ + Column {\x0a \ + id: c\ +olumn\x0a \ + spacing: 40\x0a \ + width:\ + parent.width\x0a\x0a \ + Check\ +Box {\x0a \ + id: always\ +OnCheckBox\x0a \ + width\ +: parent.width\x0a \ + t\ +ext: qsTr(\x22Alway\ +s on\x22)\x0a \ + }\x0a\x0a \ + Label {\x0a \ + width:\ + parent.width\x0a \ + wr\ +apMode: Label.Wr\ +ap\x0a \ + horizontalAli\ +gnment: Qt.Align\ +HCenter\x0a \ + text: qs\ +Tr(\x22ScrollBar is\ an interactive \ bar that can be \ used to scroll t\ @@ -1308,23 +2128,29 @@ ickable, \x22\x0a \ +\ \x22such as ListVi\ ew and GridView.\ -\x22\x0a }\x0a\ -\x0a Ima\ -ge {\x0a \ - rotation: 9\ -0\x0a \ - source: \x22../im\ -ages/arrows.png\x22\ -\x0a \ - anchors.horizon\ -talCenter: paren\ -t.horizontalCent\ -er\x0a }\ -\x0a }\x0a }\ -\x0a\x0a ScrollBar.\ -vertical: Scroll\ -Bar { }\x0a}\x0a\ -\x00\x00\x03v\ +\x22)\x0a }\ +\x0a\x0a Im\ +age {\x0a \ + rotation: \ +90\x0a \ + source: \x22../i\ +mages/arrows.png\ +\x22\x0a \ + anchors.horizo\ +ntalCenter: pare\ +nt.horizontalCen\ +ter\x0a \ +}\x0a }\x0a \ +}\x0a\x0a ScrollBar\ +.vertical: Scrol\ +lBar {\x0a p\ +olicy: alwaysOnC\ +heckBox.checked \ +? ScrollBar.Alwa\ +ysOn : ScrollBar\ +.AsNeeded\x0a }\x0a\ +}\x0a\ +\x00\x00\x03\xbf\ /\ / Copyright (C) \ 2017 The Qt Comp\ @@ -1350,39 +2176,43 @@ Wrap\x0a \ horizontalAlign\ ment: Qt.AlignHC\ enter\x0a \ - text: \x22RangeSl\ -ider is used to \ -select a range s\ -pecified by two \ -values, by slidi\ -ng each handle a\ -long a track.\x22\x0a \ - }\x0a\x0a \ - RangeSlider {\x0a\ - id: \ -slider\x0a \ - first.value: \ -0.25\x0a \ - second.value: 0\ -.75\x0a \ -anchors.horizont\ -alCenter: parent\ -.horizontalCente\ -r\x0a }\x0a\x0a \ - RangeSlider\ - {\x0a o\ -rientation: Qt.V\ -ertical\x0a \ - first.value:\ - 0.25\x0a \ - second.value: \ -0.75\x0a \ - anchors.horizon\ -talCenter: paren\ -t.horizontalCent\ -er\x0a }\x0a \ - }\x0a}\x0a\ -\x00\x00\x05\x17\ + text: qsTr(\x22Ra\ +ngeSlider is use\ +d to select a ra\ +nge specified by\ + two values, by \ +sliding each han\ +dle along a trac\ +k.\x22)\x0a }\x0a\x0a\ + RangeSli\ +der {\x0a \ + enabled: !Gall\ +eryConfig.disabl\ +ed\x0a f\ +irst.value: 0.25\ +\x0a sec\ +ond.value: 0.75\x0a\ + anch\ +ors.horizontalCe\ +nter: parent.hor\ +izontalCenter\x0a \ + }\x0a\x0a \ + RangeSlider {\x0a \ + enabl\ +ed: !GalleryConf\ +ig.disabled\x0a \ + orientat\ +ion: Qt.Vertical\ +\x0a fir\ +st.value: 0.25\x0a \ + secon\ +d.value: 0.75\x0a \ + anchor\ +s.horizontalCent\ +er: parent.horiz\ +ontalCenter\x0a \ + }\x0a }\x0a}\x0a\ +\x00\x00\x05b\ /\ / Copyright (C) \ 2017 The Qt Comp\ @@ -1416,57 +2246,62 @@ Label.Wrap\x0a \ horizonta\ lAlignment: Qt.A\ lignHCenter\x0a \ - text: \x22A\ - GroupBox provid\ -es a frame, a ti\ -tle on top of it\ -, and a logical \ -group of control\ -s within that fr\ -ame.\x22\x0a }\x0a\ -\x0a GroupBo\ -x {\x0a \ -title: \x22Title\x22\x0a \ - ancho\ -rs.horizontalCen\ -ter: parent.hori\ -zontalCenter\x0a\x0a \ - Column\ - {\x0a \ - spacing: 20\x0a \ - w\ -idth: page.itemW\ -idth\x0a\x0a \ - RadioButto\ -n {\x0a \ - text: \x22F\ -irst\x22\x0a \ - checke\ -d: true\x0a \ - widt\ -h: parent.width\x0a\ + text: qs\ +Tr(\x22A GroupBox p\ +rovides a frame,\ + a title on top \ +of it, and a log\ +ical group of co\ +ntrols within th\ +at frame.\x22)\x0a \ + }\x0a\x0a G\ +roupBox {\x0a \ + enabled: !\ +GalleryConfig.di\ +sabled\x0a \ + title: qsTr(\x22\ +Title\x22)\x0a \ + anchors.hori\ +zontalCenter: pa\ +rent.horizontalC\ +enter\x0a\x0a \ + Column {\x0a \ + spac\ +ing: 20\x0a \ + width: p\ +age.itemWidth\x0a\x0a \ + R\ +adioButton {\x0a \ \ -}\x0a \ - RadioButton {\x0a\ + text: qsTr(\x22Fir\ +st\x22)\x0a \ + checked\ +: true\x0a \ + width\ +: parent.width\x0a \ + }\ +\x0a \ + RadioButton {\x0a \ \ - id: button\x0a \ + id: button\x0a \ \ - text: \x22Second\ -\x22\x0a \ - width: par\ -ent.width\x0a \ - }\x0a \ - Radi\ -oButton {\x0a \ - te\ -xt: \x22Third\x22\x0a \ + text: qsTr(\x22Se\ +cond\x22)\x0a \ + width\ +: parent.width\x0a \ + }\ +\x0a \ + RadioButton {\x0a \ \ -width: parent.wi\ -dth\x0a \ - }\x0a \ - }\x0a }\x0a \ - }\x0a}\x0a\ -\x00\x00\x03\xc7\ + text: qsTr(\x22T\ +hird\x22)\x0a \ + width\ +: parent.width\x0a \ + }\ +\x0a }\x0a \ + }\x0a }\x0a}\ +\x0a\ +\x00\x00\x04A\ /\ / Copyright (C) \ 2017 The Qt Comp\ @@ -1492,44 +2327,52 @@ Wrap\x0a \ horizontalAlign\ ment: Qt.AlignHC\ enter\x0a \ - text: \x22RadioBu\ -tton presents an\ - option button t\ -hat can be toggl\ -ed on or off. \x22\x0a\ - \ -+ \x22Radio buttons\ - are typically u\ -sed to select on\ -e option from a \ -set of options.\x22\ -\x0a }\x0a\x0a \ - Column {\x0a \ - spacing\ -: 20\x0a \ - anchors.horizon\ -talCenter: paren\ -t.horizontalCent\ -er\x0a\x0a \ -RadioButton {\x0a \ - te\ -xt: \x22First\x22\x0a \ - }\x0a \ + text: qsTr(\x22Ra\ +dioButton presen\ +ts an option but\ +ton that can be \ +toggled on or of\ +f. \x22\x0a \ + + \x22Radio bu\ +ttons are typica\ +lly used to sele\ +ct one option fr\ +om a set of opti\ +ons.\x22)\x0a }\ +\x0a\x0a Column\ + {\x0a s\ +pacing: 20\x0a \ + anchors.h\ +orizontalCenter:\ + parent.horizont\ +alCenter\x0a\x0a \ RadioButto\ n {\x0a \ - text: \x22Secon\ -d\x22\x0a \ - checked: true\ -\x0a }\x0a \ - Radio\ -Button {\x0a \ - text: \x22\ -Third\x22\x0a \ - enabled: \ -false\x0a \ - }\x0a }\x0a \ - }\x0a}\x0a\ -\x00\x00\x04U\ + text: qsTr(\x22\ +First\x22)\x0a \ + enabled:\ + !GalleryConfig.\ +disabled\x0a \ + }\x0a \ + RadioButton {\ +\x0a \ + text: qsTr(\x22Sec\ +ond\x22)\x0a \ + checked: t\ +rue\x0a \ + enabled: !Ga\ +lleryConfig.disa\ +bled\x0a \ + }\x0a R\ +adioButton {\x0a \ + tex\ +t: qsTr(\x22Third\x22)\ +\x0a \ + enabled: false\x0a\ + }\x0a \ + }\x0a }\x0a}\x0a\ +\ +\x00\x00\x04\x7f\ /\ / Copyright (C) \ 2017 The Qt Comp\ @@ -1542,158 +2385,163 @@ mport QtQuick\x0aim\ port QtQuick.Con\ trols\x0a\x0aFlickable\ {\x0a id: flick\ -able\x0a\x0a conten\ -tHeight: pane.he\ -ight\x0a\x0a Pane {\ -\x0a id: pan\ -e\x0a width:\ - flickable.width\ -\x0a height:\ - flickable.heigh\ -t * 1.25\x0a\x0a \ - Column {\x0a \ - id: colum\ -n\x0a sp\ -acing: 40\x0a \ - width: par\ -ent.width\x0a\x0a \ - Label {\x0a \ - w\ -idth: parent.wid\ -th\x0a \ - wrapMode: Lab\ -el.Wrap\x0a \ - horizont\ -alAlignment: Qt.\ -AlignHCenter\x0a \ - tex\ -t: \x22ScrollIndica\ -tor is a non-int\ -eractive indicat\ -or that indicate\ -s the current sc\ -roll position. \x22\ -\x0a \ - + \x22A scroll\ - indicator can b\ -e either vertica\ -l or horizontal,\ - and can be atta\ -ched to any Flic\ -kable, \x22\x0a \ - + \x22\ -such as ListView\ - and GridView.\x22\x0a\ - }\x0a\x0a \ - Image\ +able\x0a enabled\ +: !GalleryConfig\ +.disabled\x0a co\ +ntentHeight: pan\ +e.height\x0a\x0a Pa\ +ne {\x0a id:\ + pane\x0a wi\ +dth: flickable.w\ +idth\x0a hei\ +ght: flickable.h\ +eight * 1.25\x0a\x0a \ + Column {\x0a \ + id: c\ +olumn\x0a \ + spacing: 40\x0a \ + width:\ + parent.width\x0a\x0a \ + Label\ {\x0a \ - rotation: 90\x0a\ - \ -source: \x22../imag\ -es/arrows.png\x22\x0a \ - a\ -nchors.horizonta\ -lCenter: parent.\ -horizontalCenter\ -\x0a }\x0a \ - }\x0a }\x0a\x0a\ - ScrollIndica\ -tor.vertical: Sc\ -rollIndicator { \ -}\x0a}\x0a\ -\x00\x00\x05\x99\ + width: parent\ +.width\x0a \ + wrapMode:\ + Label.Wrap\x0a \ + hori\ +zontalAlignment:\ + Qt.AlignHCenter\ +\x0a \ + text: qsTr(\x22Scr\ +ollIndicator is \ +a non-interactiv\ +e indicator that\ + indicates the c\ +urrent scroll po\ +sition. \x22\x0a \ + + \ +\x22A scroll indica\ +tor can be eithe\ +r vertical or ho\ +rizontal, and ca\ +n be attached to\ + any Flickable, \ +\x22\x0a \ + + \x22such as\ + ListView and Gr\ +idView.\x22)\x0a \ + }\x0a\x0a \ + Image {\x0a \ + rot\ +ation: 90\x0a \ + source\ +: \x22../images/arr\ +ows.png\x22\x0a \ + anchors\ +.horizontalCente\ +r: parent.horizo\ +ntalCenter\x0a \ + }\x0a \ + }\x0a }\x0a\x0a Sc\ +rollIndicator.ve\ +rtical: ScrollIn\ +dicator { }\x0a}\x0a\ +\x00\x00\x05\xcd\ \x00\ -\x00\x17\x96x\xda\xcdXKs\xdb6\x10\xbe\xf3W\xec\ -\xf8d'\x16\x948\x99\xe9\x8c.\x9dD\x8e'\x9e\xb1\ -\xe3W\xd2\xb43\xbd@\xc4RB\x0d\x024\x00\xdaV\ -S\xff\xf7.H=(\xf1!\xc5I\xed\xf2 \x11\xc0\ -.\xf6\xf5\xedb\xc1~\x1f\x86&\x9bZ9\x9ex\xd8\ -\x1d\xee\xc1\xc1\xab\xd7\xbf\xc0\xe7\x09\xc2\x85\xa7\x954\xe3\ -z\x0a'^\xb0\xa8\xdf\x87\xab\xf3\xc3\xdf{'2F\ -\xed\xb0w,P{\x99H\xb4\x03\x98\xcd]b\xd2\xbb\ -\xf0=bK\xd1\xc6\x92+8\xbb\x84\xf7W\x87\xbd7\ -\xbd\xa1\xe2\xb9\xc3(\x92if\xac\xa7\xcd/r\x19_\ -\xaf\x0d\xd9\x09\x9f\x9a\xdc\xbb\xf5\xe9\xa1\xd1\xde\x1a\xe5\xa2\ -\xe8*\xa6\x7f\xc5G\x0a\xcf\xf9\x18\xe1[\x04\xf4H1\ -\x80\x8c\x86Q1\xb2\xc8\x85\xd1j\x0a\x995\x19Z?\ -\x05\xa9=\x8cr\xef\x8d\xfe*\x85\x9f\x0c\xe0\x94\xfb\x09\ -K\xf9\xfdn9\xcbH\x9c\x92\xb1\xf4\xc5\xf2\xfelY\ -\xea\xc6ex\x01\x07\xfb\x858\xc6o\xb9,t)\x17\ -\xfa\xf0fo\xaf\xd4ahT\x9e\xea\x99z\xe1q\x19\ -\x8f\xa5\x1e\x0f\xe0\xed\xab\xc5\xdc]\xa9L\xc6-9\x92\ -\x15\xa3h\xb1x\xc2G\xa8*\x1b\xb41\xac\xac[\x9e\ -\x9d\x1a\x81\x83\x92\x9b}\xa5\xf1\x0a\xc1\xc4X\xf97\xf9\ -\x92\xabwJ\x8euJ\xdb\x0c\xc8\xc9\xac\x18}\x1c\xd2\ -\x10\xed\x0a\x83\xc7{\xa2\xd89\xa4P\x9a1H\x07\x1c\ -2\x93\xe5\x19\xf8\x09\xf7a\x9c\x1a\xe7\xc9\xd5\x14Z\x01\ -\x89\xb1\xe0H\x84\xef\xd16)x\xee\xae\x1d\xec\xac\xec\ -\x17\x9e\x97\xb0\xc3\xb5\x80\x91\x95\x98@LP\xc9\xb5\x8c\ -\xb9\x97F;\xb2\x90\xfc\xe8\x09|\xb4\xa1eK\xde\x87\ -\xa5c\xde\x171Y\xf3\xccL\xcfSt\x8e\x02\xb3*\ -\x93\xeb\x98\x94rli|i\xe8\xc2\x91\xeb\x0b\xcd>\ -\xa7xW0\xb4Bc\xf4\x90\xf0q\x8d\x04\xc3\xb4\xd4\ -\xa0t\x18#\xfc\xe9\xdd\xbdh\x85x\xe6\xcbo5\xbf\ -\xc8u\xf6\xa8Fr?\x80\xddj\xf4\xa1W\xea\xb7G\ -\xd8;\xa8QO\x97\xd4\x13,\xf2\xbb\x07\xe5KI_\ -c\xf0\xd2+\xac\xba\xb1F\xd1\x84\xca\xb5\x18\x9c\x18\x8b\ -)\xc8\xcc\xe5)\x08\xa3\x02&\xa4\x07\x9e\xa2g\x8c\xd5\ -\xd1\xf0\x105\x8f6E\x93\ -\xf8#\x89Jt\x98\xd4\x15\xf1\x05|B\xb9\x9f\x18%\ -\xd0~.-\xfaBM\x93\xa6\x83\xfc\x99\x95\xaf+v\ -\xce\x9d\xbb3V\xb4+\x86\x84\x85\xf2TYH`s\ -\xae\x0f\xb4v\xa6?\x88\x8e6\xf0\x87\xecj/\x7f\xe5\ -\xefC\xf4/yY\x0ax\ -\x00\x00\x03\xe0\ +\x00\x18\xbax\x9c\xcdXKo\x1b7\x10\xbe\xebWL\ +}\xb2\x1b\x8bJ\xec\x02\x05t)\x129i\x0c\xd8\xb1\ +\x13;M\x0b\xf4B\xed\x8e$\xd6\x5crMrm+\ +\xa9\xff{\x87\xa4\x1e\xab}IH\xda:s\x90\x96\xcb\ +\x19\xce\xeb\xe3p\xb8\x83\x01\x8ct>7b:s\xb0\ +?:\x80\xa3\xe7/~\x86\xeb\x19\xc2{G3Y\xce\ +\xd5\x1c\xce\x5c\xcaz\x83\x01\x5c]\x9e\xfc\xde?\x13\x09\ +*\x8b\xfd\xd3\x14\x95\x13\x13\x81f\x08\x8bw\x1fp\xd2\ +\x7f\xef\xfa$\x96\xa1I\x04\x97p\xf1\x01^]\x9d\xf4\ +\x8f\xfb#\xc9\x0b\x8b\xbd\x9e\xc8rm\x1c-\xfe\xbe\x10\ +\xc9Me\xc8\xce\xf8\x5c\x17\xceV_\x8f\xb4rFK\ +\xdb\xeb]%\xf4/\xf9X\xe2%\x9f\x22|\xe9\x01\x91\ +H\x87\x90\xd3\xb0\x17F\x06y\xaa\x95\x9cCnt\x8e\ +\xc6\xcdA(\x07\xe3\xc29\xad>\x89\xd4\xcd\x86p\xce\ +\xdd\x8ce\xfca?\xbee\xa4N\x8aD\xb80}\xb8\ +\x98\x16\xaaq\x1a~\x84\xa3\xc3\xa0\x8e\xf1;.\x82-\ +qb\x00\xc7\x07\x07\xd1\x86\x91\x96E\xa6\x16\xe6y\xb2\ +9O\x84\x9a\x0e\xe1\xa7\xe7\xabw\xf7\xd1\x98\x9c\x1b\x0a\ +$\x0b\xa3\xdej\xf2\x8c\x8fQ\x96\x16h\x13\xd8\x987\ +\xc4\x87\xc8_\x13p\xc2\ +I\xacG\xbf\xc6\xd7\x04\xec\xd5\x1a\xa5\x04\x9ei\x83\x19\ +\x88\xdc\x16\x19\xa4ZzX\x09\x07\xd41\x99\xadX\x0a\xa1\ +5Y\x80\xe7S\x02*)\xd9\xf1\xb5\xa8\xaa\xaf\xf1\xdd\ +A\xab\xca\x1fy\x87pq\x87F\xf29\xd3\xf1\xbfn\ +F\xa6S.\x87\xe0L\x81\xdd\xf0\xecH\xa8'\xeb\xa8\ +6q\x93F\xbc\xd8\xe1\x22\xa2\xec\x0f\xb4\xf0\xf7r\xf0\ +N\xd7\x0d\xa8\x95\xfd\x8dU\x97G\xc0\xd1\xf3\xc6\xf9%\ +\x94&B\xca%|\x1a\x19\xbbvPp\xb5\x04]\x7f\ +\x90\xa7:)|\xa9\x87\x19\xb70FT>P\xfe\xe0\ +N\xd9\x9f\xeaD\x03\x1d\xb9p\xcfi\xdei\xb0\xfc\x0e\ +\xfd\x0b\x03\xc9\x8c\xab)\xda_\x1a\xe2\xe3\xe9\xb1\xf1\xed\ +h\x86\xc9\xcd+\xfd\xb0\xa3u\xa4\x5ci\xda\xd4\xf6\x06\ +\xf8\x94\x8b\xa6\x5cT\xa3\x13\x1a\x95\xd5\xee\x0a\xa3\x1d\xed\ +\xfb\xda\x02Q\xa9\x04\x8e\xf4>q\x11\xf0&|\xc3\xfe\ +_\x8b\x7f\xf7[\xff\xbe\xdc\xafQC\x16\x82t\x1f\xbb\ +\xb4\xf0\xbc\x16>\xf6\x8dYm\x81\x85\xbbo1\xc2\x86\ +|\xd6K\xcdM\xec\xffI\xa9i@\x8c\xa7\xb6*3\ +\x92\xdab]\xe3\x1b\x0f\x00\x9f\x84\x96\xbd\xe5\x93;Y\ +\xf24r$R\xe4-&{\xda\xb9\xfeTb\x9a\x84\ +\xa2\xb7\x88j\xddnO\x9duqi|\x5c\xa7\x95e\ +[\xf9\xf4\xd4\xd9QW\xe94[\xdf \xba\xec\xf2\x98\ +\xe9dj\xd0\xda\x08\xe62}[\xc9\xa8\x92OYl\ +\xfc\x83S\xec\xd2 \xb5\xcew\xf8\xd2\xe6\x98\xb87\xa2\ +9\x8fK\xb2T\xee\x13\x92\xddcl \xbc\xbc\x1d\xdc\ +\xba~\xd8*\xb9\x9a\xd6{\xf7%=\xb6\xc7v\xdb\x11\ +\xe5i\xdbm\xa6J\xbb5\x87\x87\x1e\x9e\x96\xbcFG\ +\x87\x18OE.\xac\xc7\x0d\xa0\x14\x8e\xc1\xbbB%\x14\ +.%\xc6E\xd3\xb5\xa4JtM\x11\x0a\xd0:\xb8-\ +\xe8\xa2#9\xe9\xa5\x06\x14N}JRR\x8e\x0e2\ +.\xd1\x16<\xe50!\x03\xe8v\x94Pv\x1d.\xac\ +\xcb\x8d\xc8\xc4\xee\xaa&\xbcH\xbcm\x0cF\x85\xe1c\ +\xe1\x9d\xc0)i\xf9\xab\xb0\xfex\xa6\xcb\xaf\xbf\x89\x16\ +\xa97K\x89l%\x00c1FE&\xb1\xddt]\ +\x15\x84\x0dEu\xdd\x22\xdd\xe6\xfc~\x16\x0c~#O\ +i1\xda\x87\x90\x14\xc6\xd2\xaa!\x98\xb7\x05\xf7n\x16\ +\x86\x1c\x11\xe9\x86\x11;j;\xf1\xd1\x9b\xd0\x06\x96\xc2\ +\xd2\xd3\x8c4\x1b4\x94\xb2R\xb2\xca.[\x9e\x0b\xea\ +S\x1c\xdd#\xb59\x04\x9cL\x82B\xb3c\x1cS\xa0\ +\xa8\x90$(L\xca\xd2\x14G%\xacd\xf0\xd1\xc1\x1d\ +*:\xed\x1cY\x83F\x93_\x0a\x94\xc7F.y\x82\ +\x86\x13\x8evR\x85\x05p)(@YL\x07\xc5,\ +!\xc8\xb9\xa6\x1bJ\x99\xb6^\xd5\xcb\xd4\xdck\xb5l\ +\xbf\xf8\x85\xe4\x94RK\xf7im\x18\x9d`\x8e\x1e\xa9\ +\xa4Wf:\xb6\xe7\xf2\x18\xdc\xec5\x16\xa3S\x87\xd9\ +\xd66\xcd\xe9\xbct\x1c\xf9\xe1V\x91\xb1\xa6s0+\ +K\xc57\xffZKX\x939\xe7f*\xd4\x10\xfa\x9b\ +~\x86\xb9K\x9e\xa6\xbel<\x83\x17\xffc\x7fy\xaa\ +\xf2\xe2I\xbbK\xe1\x0d\xf8\xda\xde\xb2$\xfc\xddw\x96\ +;wz\x13\xba=\xd9\x96\xb6i\xe7.\xb0)\xaf\x9e\ +\xdaz\xc0\x8b\x9b\xf5EsD\x00@\xd9v\xd9\x8c_\ +L\x9f\xfa\xcaI\x87\xeb\xba\x96\xbd\xf6\x83\x0f\x9d\xdb\xb0\ +\x8c\xf9K\x89\x9c\x0e\xa1\x00\xdd\xf0\xd9/1\x18>-\ +si\x87\x1de4z\x1el_|\xd6mmm\x9b\ +\x0b\xe85\x19\xf1F\xa0L;\x1c\xeb\xca\xfe\x92\xc2\x91\ +1\xd32Es]\xf2\xeb#\xb5`\x8a:\x82\xa7v\ +\xa1\xc5\xbcKn\xed\xbd6i\x87yH\xe8\x88\x87\xd4\ +J\x0f[\x8a\xbd\xa6\xb9\x0b\xf5:\xed\xe8-\xbf\xc9\xbb\ +\xf6j\x1a\x7f\x1f{\xff\x00\x94ma\x98\ +\x00\x00\x03\xd4\ /\ / Copyright (C) \ 2017 The Qt Comp\ @@ -1719,45 +2567,45 @@ Wrap\x0a \ horizontalAlign\ ment: Qt.AlignHC\ enter\x0a \ - text: \x22CheckBo\ -x presents an op\ -tion button that\ - can be toggled \ -on or off. \x22\x0a \ - + \x22\ -Check boxes are \ -typically used t\ -o select one or \ -more options fro\ -m a set of optio\ -ns.\x22\x0a }\x0a\x0a\ - Column {\ -\x0a spa\ -cing: 20\x0a \ - anchors.hor\ -izontalCenter: p\ -arent.horizontal\ -Center\x0a\x0a \ - CheckBox {\x0a \ - t\ -ext: \x22First\x22\x0a \ - che\ -cked: true\x0a \ - }\x0a \ - CheckBox {\x0a\ - \ -text: \x22Second\x22\x0a \ - }\x0a \ - CheckBo\ -x {\x0a \ - text: \x22Third\ + text: qsTr(\x22Ch\ +eckBox presents \ +an option button\ + that can be tog\ +gled on or off. \ \x22\x0a \ - checked: true\x0a\ - \ -enabled: false\x0a \ - }\x0a \ - }\x0a }\x0a}\x0a\ -\x00\x00\x02\xb5\ + + \x22Check boxes\ + are typically u\ +sed to select on\ +e or more option\ +s from a set of \ +options.\x22)\x0a \ + }\x0a\x0a Co\ +lumn {\x0a \ + spacing: 20\x0a \ + ancho\ +rs.horizontalCen\ +ter: parent.hori\ +zontalCenter\x0a\x0a \ + CheckB\ +ox {\x0a \ + enabled: !G\ +alleryConfig.dis\ +abled\x0a \ + text: qsTr\ +(\x22First\x22)\x0a \ + checke\ +d: true\x0a \ + }\x0a \ + CheckBox {\x0a \ + ena\ +bled: !GalleryCo\ +nfig.disabled\x0a \ + te\ +xt: qsTr(\x22Second\ +\x22)\x0a }\ +\x0a }\x0a }\ +\x0a}\x0a\ +\x00\x00\x02\xee\ /\ / Copyright (C) \ 2017 The Qt Comp\ @@ -1783,27 +2631,30 @@ Wrap\x0a \ horizontalAlign\ ment: Qt.AlignHC\ enter\x0a \ - text: \x22DelayBu\ -tton is a checka\ -ble button that \ -incorporates a d\ -elay before the \ -\x22\x0a \ - + \x22button is a\ -ctivated. This d\ -elay prevents ac\ -cidental presses\ -.\x22\x0a }\x0a\x0a \ - DelayButto\ -n {\x0a \ -text: \x22DelayButt\ -on\x22\x0a \ -anchors.horizont\ -alCenter: parent\ + text: qsTr(\x22De\ +layButton is a c\ +heckable button \ +that incorporate\ +s a delay before\ + the \x22\x0a \ + + \x22button\ + is activated. T\ +his delay preven\ +ts accidental pr\ +esses.\x22)\x0a \ + }\x0a\x0a Dela\ +yButton {\x0a \ + enabled: !\ +GalleryConfig.di\ +sabled\x0a \ + text: qsTr(\x22D\ +elayButton\x22)\x0a \ + anchors\ .horizontalCente\ -r\x0a }\x0a \ -}\x0a}\x0a\ -\x00\x00\x07\x9f\ +r: parent.horizo\ +ntalCenter\x0a \ + }\x0a }\x0a}\x0a\ +\x00\x00\x07\xe1\ /\ / Copyright (C) \ 2017 The Qt Comp\ @@ -1819,114 +2670,119 @@ mport QtQuick.Co\ ntrols\x0a\x0aStackVie\ w {\x0a id: stac\ kView\x0a initia\ -lItem: page\x0a\x0a \ - Component {\x0a \ - id: page\x0a\x0a \ - Pane {\x0a \ - id: pa\ -ne\x0a w\ -idth: parent ? p\ -arent.width : 0 \ -// TODO: fix nul\ -l parent on dest\ -ruction\x0a\x0a \ - Column {\x0a \ - sp\ -acing: 40\x0a \ - width:\ - parent.width\x0a\x0a \ - L\ -abel {\x0a \ - width\ -: parent.width\x0a \ +lItem: page\x0a \ +enabled: !Galler\ +yConfig.disabled\ +\x0a\x0a Component \ +{\x0a id: pa\ +ge\x0a\x0a Pane\ + {\x0a i\ +d: pane\x0a \ + width: paren\ +t ? parent.width\ + : 0 // TODO: fi\ +x null parent on\ + destruction\x0a\x0a \ + Column\ + {\x0a \ + spacing: 40\x0a \ + w\ +idth: parent.wid\ +th\x0a\x0a \ + Label {\x0a \ \ - wrapMode: Lab\ -el.Wrap\x0a \ - hori\ -zontalAlignment:\ - Qt.AlignHCenter\ -\x0a \ - text: \x22Stac\ -kView provides a\ - stack-based nav\ -igation model wh\ -ich can be used \ -with a set of in\ -terlinked pages.\ - \x22\x0a \ - + \x22Items \ -are pushed onto \ -the stack as the\ - user navigates \ -deeper into the \ -material, and po\ -pped off again \x22\ -\x0a \ - + \x22when he \ -chooses to go ba\ -ck.\x22\x0a \ - }\x0a\x0a \ - Button {\ -\x0a \ - id: button\x0a\ +width: parent.wi\ +dth\x0a \ + wrapMode\ +: Label.Wrap\x0a \ \ - text: \x22Push\x22\ -\x0a \ - anchors.hor\ -izontalCenter: p\ -arent.horizontal\ -Center\x0a \ - width\ -: Math.max(butto\ -n.implicitWidth,\ - Math.min(button\ -.implicitWidth *\ - 2, pane.availab\ -leWidth / 3))\x0a \ + horizontalAlign\ +ment: Qt.AlignHC\ +enter\x0a \ + text: \ +qsTr(\x22StackView \ +provides a stack\ +-based navigatio\ +n model which ca\ +n be used with a\ + set of interlin\ +ked pages. \x22\x0a \ \ - onClicked: sta\ -ckView.push(page\ -)\x0a \ - }\x0a\x0a \ - Button {\x0a \ + + \x22Items are pu\ +shed onto the st\ +ack as the user \ +navigates deeper\ + into the materi\ +al, and popped o\ +ff again \x22\x0a \ + +\ + \x22when he choose\ +s to go back.\x22)\x0a\ \ - text: \x22Pop\x22\x0a \ +}\x0a\x0a \ + Button {\x0a \ \ - enabled: stack\ -View.depth > 1\x0a \ +id: button\x0a \ + t\ +ext: qsTr(\x22Push\x22\ +)\x0a \ + anchors.ho\ +rizontalCenter: \ +parent.horizonta\ +lCenter\x0a \ + widt\ +h: Math.max(butt\ +on.implicitWidth\ +, Math.min(butto\ +n.implicitWidth \ +* 2, pane.availa\ +bleWidth / 3))\x0a \ \ - width: Math.m\ -ax(button.implic\ -itWidth, Math.mi\ -n(button.implici\ -tWidth * 2, pane\ -.availableWidth \ -/ 3))\x0a \ - anchor\ -s.horizontalCent\ -er: parent.horiz\ -ontalCenter\x0a \ + onClicked: st\ +ackView.push(pag\ +e)\x0a \ + }\x0a\x0a \ + Button {\x0a \ \ -onClicked: stack\ -View.pop()\x0a \ - }\x0a\x0a \ - La\ -bel {\x0a \ + text: qsTr(\x22P\ +op\x22)\x0a \ + enabled\ +: stackView.dept\ +h > 1\x0a \ width:\ - parent.width\x0a \ + Math.max(button\ +.implicitWidth, \ +Math.min(button.\ +implicitWidth * \ +2, pane.availabl\ +eWidth / 3))\x0a \ \ - wrapMode: Labe\ -l.Wrap\x0a \ - horiz\ -ontalAlignment: \ -Qt.AlignHCenter\x0a\ + anchors.horizon\ +talCenter: paren\ +t.horizontalCent\ +er\x0a \ + onClicked\ +: stackView.pop(\ +)\x0a \ + }\x0a\x0a \ + Label {\x0a \ + \ + width: parent.w\ +idth\x0a \ + wrapMod\ +e: Label.Wrap\x0a \ \ - text: \x22Stack\ - Depth: \x22 + stac\ -kView.depth\x0a \ + horizontalAlig\ +nment: Qt.AlignH\ +Center\x0a \ + text:\ + qsTr(\x22Stack Dep\ +th:\x22) + \x22 \x22 + st\ +ackView.depth\x0a \ + }\x0a\ }\x0a \ - }\x0a \ - }\x0a }\x0a}\x0a\ + }\x0a }\x0a}\x0a\ +\ \x00\x00\x01?\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -4086,14 +4942,23 @@ qt_resource_name = b"\ \x00P\ \x00r\x00o\x00g\x00r\x00e\x00s\x00s\x00B\x00a\x00r\x00P\x00a\x00g\x00e\x00.\x00q\ \x00m\x00l\ +\x00\x13\ +\x08\xce#\x1c\ +\x00S\ +\x00e\x00a\x00r\x00c\x00h\x00F\x00i\x00e\x00l\x00d\x00P\x00a\x00g\x00e\x00.\x00q\ +\x00m\x00l\ +\x00\x10\ +\x06\xd3\x8b\x1c\ +\x00T\ +\x00e\x00x\x00t\x00A\x00r\x00e\x00a\x00P\x00a\x00g\x00e\x00.\x00q\x00m\x00l\ \x00\x0f\ \x0b\xe33|\ \x00T\ \x00o\x00o\x00l\x00T\x00i\x00p\x00P\x00a\x00g\x00e\x00.\x00q\x00m\x00l\ -\x00\x10\ -\x06\xd3\x8b\x1c\ +\x00\x0f\ +\x0b\xc87|\ \x00T\ -\x00e\x00x\x00t\x00A\x00r\x00e\x00a\x00P\x00a\x00g\x00e\x00.\x00q\x00m\x00l\ +\x00o\x00o\x00l\x00B\x00a\x00r\x00P\x00a\x00g\x00e\x00.\x00q\x00m\x00l\ \x00\x0e\ \x02%\xd0|\ \x00S\ @@ -4102,10 +4967,34 @@ qt_resource_name = b"\ \x00\xf4\xb9\xfc\ \x00T\ \x00u\x00m\x00b\x00l\x00e\x00r\x00P\x00a\x00g\x00e\x00.\x00q\x00m\x00l\ +\x00\x10\ +\x02a.\x1c\ +\x00T\ +\x00r\x00e\x00e\x00V\x00i\x00e\x00w\x00P\x00a\x00g\x00e\x00.\x00q\x00m\x00l\ +\x00\x11\ +\x0fX4\x5c\ +\x00M\ +\x00o\x00n\x00t\x00h\x00G\x00r\x00i\x00d\x00P\x00a\x00g\x00e\x00.\x00q\x00m\x00l\ +\ +\x00\x11\ +\x05\x22\x04\xdc\ +\x00S\ +\x00p\x00l\x00i\x00t\x00V\x00i\x00e\x00w\x00P\x00a\x00g\x00e\x00.\x00q\x00m\x00l\ +\ +\x00\x11\ +\x03\x22,\x5c\ +\x00T\ +\x00a\x00b\x00l\x00e\x00V\x00i\x00e\x00w\x00P\x00a\x00g\x00e\x00.\x00q\x00m\x00l\ +\ \x00\x0e\ \x0e\xa2\x84\x9c\ \x00B\ \x00u\x00t\x00t\x00o\x00n\x00P\x00a\x00g\x00e\x00.\x00q\x00m\x00l\ +\x00\x11\ +\x04\xf7 \x1c\ +\x00G\ +\x00a\x00l\x00l\x00e\x00r\x00y\x00C\x00o\x00n\x00f\x00i\x00g\x00.\x00q\x00m\x00l\ +\ \x00\x0e\ \x0b\xc5|\x5c\ \x00S\ @@ -4131,6 +5020,10 @@ qt_resource_name = b"\ \x0c\xc8%\xdc\ \x00F\ \x00r\x00a\x00m\x00e\x00P\x00a\x00g\x00e\x00.\x00q\x00m\x00l\ +\x00\x0f\ +\x0c\xe8\x19\xfc\ +\x00M\ +\x00e\x00n\x00u\x00B\x00a\x00r\x00P\x00a\x00g\x00e\x00.\x00q\x00m\x00l\ \x00\x11\ \x03$Q\x5c\ \x00S\ @@ -4267,138 +5160,154 @@ qt_resource_name = b"\ qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x08\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x001\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x009\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\xb4\x00\x02\x00\x00\x00\x1b\x00\x00\x00\x16\ +\x00\x00\x00\xb4\x00\x02\x00\x00\x00#\x00\x00\x00\x16\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x90\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x0a\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\xa2\x00\x00\x00\x00\x00\x01\x00\x00\x0a\xc1\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x00,\x00\x00\x00\x00\x00\x01\x00\x00\x09\xc5\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x00t\x00\x00\x00\x00\x00\x01\x00\x00\x0a)\ -\x00\x00\x01\x86\xb7m\x07\x8b\ +\x00\x00\x00\xa2\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xbc\ +\x00\x00\x01\x99{\x95\xd3\xc8\ +\x00\x00\x00,\x00\x00\x00\x00\x00\x01\x00\x00\x0b\xc0\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x00t\x00\x00\x00\x00\x00\x01\x00\x00\x0c$\ +\x00\x00\x01\x99\xa0\x15<\x1b\ \x00\x00\x00\x5c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x09\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x10\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x86\xcc\xe0s'\ -\x00\x00\x00t\x00\x00\x00\x00\x00\x01\x00\x00\xe93\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x05\xf6\x00\x00\x00\x00\x00\x01\x00\x00\xd5\x9d\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x062\x00\x00\x00\x00\x00\x01\x00\x00\xe5\x81\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x058\x00\x00\x00\x00\x00\x01\x00\x00\x9b\xf7\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x06\x16\x00\x00\x00\x00\x00\x01\x00\x00\xda\x5c\ -\x00\x00\x01\x87\x137-\xff\ -\x00\x00\x04\xfc\x00\x00\x00\x00\x00\x01\x00\x00u\x04\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x04\xe4\x00\x00\x00\x00\x00\x01\x00\x00s\xc1\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x05\xd4\x00\x00\x00\x00\x00\x01\x00\x00\xc2Q\ -\x00\x00\x01\x87\x137\xd0\xfa\ -\x00\x00\x05\x94\x00\x00\x00\x00\x00\x01\x00\x00\xa4\x17\ -\x00\x00\x01\x87\x137\xd0\xfa\ -\x00\x00\x05\x16\x00\x00\x00\x00\x00\x01\x00\x00v\xd2\ -\x00\x00\x01\x87\x137\xd0\xfe\ -\x00\x00\x05\xb6\x00\x00\x00\x00\x00\x01\x00\x00\xc0q\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x05X\x00\x00\x00\x00\x00\x01\x00\x00\x9e\xb6\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x05v\x00\x00\x00\x00\x00\x01\x00\x00\xa1\xb0\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x03\x96\x00\x00\x00\x00\x00\x01\x00\x00O+\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x02\xe2\x00\x01\x00\x00\x00\x01\x00\x009\x1c\ -\x00\x00\x01\x86\xcc\xe0s'\ -\x00\x00\x03n\x00\x00\x00\x00\x00\x01\x00\x00J\xec\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x02,\x00\x00\x00\x00\x00\x01\x00\x00&\x88\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x02\x0a\x00\x00\x00\x00\x00\x01\x00\x00#\x85\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xdf\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x03F\x00\x00\x00\x00\x00\x01\x00\x00E\xa6\ -\x00\x00\x01\x86\xcc\xe0s'\ -\x00\x00\x04j\x00\x00\x00\x00\x00\x01\x00\x00e\x81\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x01j\x00\x00\x00\x00\x00\x01\x00\x00\x18\xb0\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x04\x90\x00\x00\x00\x00\x00\x01\x00\x00ie\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x02\xbc\x00\x00\x00\x00\x00\x01\x00\x003L\ -\x00\x00\x01\x86\xcc\xe0s'\ -\x00\x00\x01\xe4\x00\x00\x00\x00\x00\x01\x00\x00 \xe6\ -\x00\x00\x01\x86\xcc\xe0s'\ -\x00\x00\x04\xbc\x00\x00\x00\x00\x00\x01\x00\x00l\x1e\ -\x00\x00\x01\x86\xcc\xe0s'\ -\x00\x00\x00\xc4\x00\x00\x00\x00\x00\x01\x00\x00\x0a\xd0\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x04H\x00\x01\x00\x00\x00\x01\x00\x00_\xe4\ -\x00\x00\x01\x86\xcc\xe0s'\ -\x00\x00\x01\x18\x00\x00\x00\x00\x00\x01\x00\x00\x10g\ -\x00\x00\x01\x86\xcc\xe0s'\ -\x00\x00\x04\x14\x00\x00\x00\x00\x00\x01\x00\x00[\x8b\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x02r\x00\x00\x00\x00\x00\x01\x00\x00->\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x01\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x1e\x08\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x03&\x00\x00\x00\x00\x00\x01\x00\x00@\xba\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x01\x94\x00\x00\x00\x00\x00\x01\x00\x00\x1a\xb5\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x03\x08\x00\x00\x00\x00\x00\x01\x00\x00=\xee\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x02P\x00\x00\x00\x00\x00\x01\x00\x00(\xd1\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x01:\x00\x00\x00\x00\x00\x01\x00\x00\x15\xe6\ -\x00\x00\x01\x86\xcc\xe0s'\ -\x00\x00\x03\xe8\x00\x00\x00\x00\x00\x01\x00\x00W\xc0\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x03\xc2\x00\x00\x00\x00\x00\x01\x00\x00R\xa5\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x02\x94\x00\x00\x00\x00\x00\x01\x00\x000\xe7\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x06R\x00\x02\x00\x00\x00\x05\x00\x00\x002\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x00t\x00\x00\x00\x00\x00\x01\x00\x01\x1d|\ +\x00\x00\x01\x99\xa0\x15<\x1b\ +\x00\x00\x070\x00\x00\x00\x00\x00\x01\x00\x01\x09\xe6\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x07l\x00\x00\x00\x00\x00\x01\x00\x01\x19\xca\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x06r\x00\x00\x00\x00\x00\x01\x00\x00\xd0@\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x07P\x00\x00\x00\x00\x00\x01\x00\x01\x0e\xa5\ +\x00\x00\x01\x87,\x09\xa2X\ +\x00\x00\x066\x00\x00\x00\x00\x00\x01\x00\x00\xa9M\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x06\x1e\x00\x00\x00\x00\x00\x01\x00\x00\xa8\x0a\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x07\x0e\x00\x00\x00\x00\x00\x01\x00\x00\xf6\x9a\ +\x00\x00\x01\x87,\x09\xa2X\ +\x00\x00\x06\xce\x00\x00\x00\x00\x00\x01\x00\x00\xd8`\ +\x00\x00\x01\x87,\x09\xa2X\ +\x00\x00\x06P\x00\x00\x00\x00\x00\x01\x00\x00\xab\x1b\ +\x00\x00\x01\x87,\x09\xa2X\ +\x00\x00\x06\xf0\x00\x00\x00\x00\x00\x01\x00\x00\xf4\xba\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x06\x92\x00\x00\x00\x00\x00\x01\x00\x00\xd2\xff\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x06\xb0\x00\x00\x00\x00\x00\x01\x00\x00\xd5\xf9\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x04\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x81\x99\ +\x00\x00\x01\x99\xa0\x15<\x1d\ +\x00\x00\x03\xf8\x00\x01\x00\x00\x00\x01\x00\x00d\xea\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x04\xa8\x00\x00\x00\x00\x00\x01\x00\x00|B\ +\x00\x00\x01\x99\xa0\x15<\x1d\ +\x00\x00\x02|\x00\x00\x00\x00\x00\x01\x00\x002\x85\ +\x00\x00\x01\x99\xa0\x15<\x1e\ +\x00\x00\x02Z\x00\x00\x00\x00\x00\x01\x00\x00/9\ +\x00\x00\x01\x99\xa0\x15<\x1d\ +\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x10-\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x02\xa0\x00\x00\x00\x00\x00\x01\x00\x005\x01\ +\x00\x00\x01\x99\xa0\x15<\x1e\ +\x00\x00\x03\x16\x00\x00\x00\x00\x00\x01\x00\x00K\xab\ +\x00\x00\x01\x99\xa0\x15<\x1e\ +\x00\x00\x04\x80\x00\x00\x00\x00\x00\x01\x00\x00v\xcd\ +\x00\x00\x01\x99\xa0\x15<\x1d\ +\x00\x00\x05\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x99[\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x03`\x00\x00\x00\x00\x00\x01\x00\x00W\x8a\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x02\xee\x00\x00\x00\x00\x00\x01\x00\x00C\xe3\ +\x00\x00\x01\x99\xa0\x15<\x1d\ +\x00\x00\x01j\x00\x00\x00\x00\x00\x01\x00\x00\x1bG\ +\x00\x00\x01\x99\xa0\x15<\x1d\ +\x00\x00\x05\xca\x00\x00\x00\x00\x00\x01\x00\x00\x9d3\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x03\xd2\x00\x00\x00\x00\x00\x01\x00\x00^\x93\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x01\xec\x00\x00\x00\x00\x00\x01\x00\x00'0\ +\x00\x00\x01\x99\xa0\x15<\x1e\ +\x00\x00\x05\xf6\x00\x00\x00\x00\x00\x01\x00\x00\xa0%\ +\x00\x00\x01\x99\xa0\x15<\x1d\ +\x00\x00\x00\xc4\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xff\ +\x00\x00\x01\x99\xa0\x15<\x1d\ +\x00\x00\x01\xc0\x00\x00\x00\x00\x00\x01\x00\x00 \xa5\ +\x00\x00\x01\x99\xa0\x15<\x1d\ +\x00\x00\x05\x82\x00\x01\x00\x00\x00\x01\x00\x00\x93\x8a\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x01\x18\x00\x00\x00\x00\x00\x01\x00\x00\x12\xbb\ +\x00\x00\x01\x99\xa0\x15<\x1e\ +\x00\x00\x05N\x00\x00\x00\x00\x00\x01\x00\x00\x8f\x07\ +\x00\x00\x01\x99\xa0\x15<\x1d\ +\x00\x00\x03\x88\x00\x00\x00\x00\x00\x01\x00\x00XN\ +\x00\x00\x01\x99\xa0\x15<\x1d\ +\x00\x00\x026\x00\x01\x00\x00\x00\x01\x00\x00,\xf8\ +\x00\x00\x01\x99\xa0\x15<\x1e\ +\x00\x00\x02\x12\x00\x00\x00\x00\x00\x01\x00\x00*\x08\ +\x00\x00\x01\x99\xa0\x15<\x1e\ +\x00\x00\x04<\x00\x00\x00\x00\x00\x01\x00\x00l\xd7\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x04\x5c\x00\x00\x00\x00\x00\x01\x00\x00r\x08\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x01\x94\x00\x00\x00\x00\x00\x01\x00\x00\x1dL\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x04\x1e\x00\x00\x00\x00\x00\x01\x00\x00i\xd8\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x03>\x00\x01\x00\x00\x00\x01\x00\x00UB\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x01:\x00\x00\x00\x00\x00\x01\x00\x00\x18w\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x05\x22\x00\x00\x00\x00\x00\x01\x00\x00\x8a\xc2\ +\x00\x00\x01\x99\xa0\x15<\x1d\ +\x00\x00\x02\xc6\x00\x01\x00\x00\x00\x01\x00\x00@\xb9\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x04\xfc\x00\x00\x00\x00\x00\x01\x00\x00\x85\x5c\ +\x00\x00\x01\x99\xa0\x15<\x1c\ +\x00\x00\x03\xaa\x00\x00\x00\x00\x00\x01\x00\x00\x5c\x0b\ +\x00\x00\x01\x99\xa0\x15<\x1e\ +\x00\x00\x07\x8c\x00\x02\x00\x00\x00\x05\x00\x00\x00:\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x06z\x00\x02\x00\x00\x00\x03\x00\x00\x00@\ +\x00\x00\x07\xb4\x00\x02\x00\x00\x00\x03\x00\x00\x00H\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x06f\x00\x02\x00\x00\x00\x03\x00\x00\x00=\ +\x00\x00\x07\xa0\x00\x02\x00\x00\x00\x03\x00\x00\x00E\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x06\xba\x00\x02\x00\x00\x00\x03\x00\x00\x00:\ +\x00\x00\x07\xf4\x00\x02\x00\x00\x00\x03\x00\x00\x00B\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x06\x8a\x00\x02\x00\x00\x00\x03\x00\x00\x007\ +\x00\x00\x07\xc4\x00\x02\x00\x00\x00\x03\x00\x00\x00?\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x06\x9e\x00\x00\x00\x00\x00\x01\x00\x00\xe9\xf6\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x06\xce\x00\x00\x00\x00\x00\x01\x00\x00\xed6\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x06\xfe\x00\x00\x00\x00\x00\x01\x00\x00\xee\xb7\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x06\xe4\x00\x00\x00\x00\x00\x01\x00\x00\xee0\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x06\xce\x00\x00\x00\x00\x00\x01\x00\x00\xeb\x04\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x06\xfe\x00\x00\x00\x00\x00\x01\x00\x00\xecq\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x06\xe4\x00\x00\x00\x00\x00\x01\x00\x00\xeb\xeb\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x06\xce\x00\x00\x00\x00\x00\x01\x00\x00\xf1.\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x06\xfe\x00\x00\x00\x00\x00\x01\x00\x00\xf2l\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x06\xe4\x00\x00\x00\x00\x00\x01\x00\x00\xf1\xea\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x06\xce\x00\x00\x00\x00\x00\x01\x00\x00\xef\x9a\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x06\xfe\x00\x00\x00\x00\x00\x01\x00\x00\xf0\xaf\ -\x00\x00\x01\x86\xb7m\x07\x8b\ -\x00\x00\x06\xe4\x00\x00\x00\x00\x00\x01\x00\x00\xf00\ -\x00\x00\x01\x86\xb7m\x07\x8b\ +\x00\x00\x07\xd8\x00\x00\x00\x00\x00\x01\x00\x01\x1e?\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x08\x08\x00\x00\x00\x00\x00\x01\x00\x01!\x7f\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x088\x00\x00\x00\x00\x00\x01\x00\x01#\x00\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x08\x1e\x00\x00\x00\x00\x00\x01\x00\x01\x22y\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x08\x08\x00\x00\x00\x00\x00\x01\x00\x01\x1fM\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x088\x00\x00\x00\x00\x00\x01\x00\x01 \xba\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x08\x1e\x00\x00\x00\x00\x00\x01\x00\x01 4\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x08\x08\x00\x00\x00\x00\x00\x01\x00\x01%w\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x088\x00\x00\x00\x00\x00\x01\x00\x01&\xb5\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x08\x1e\x00\x00\x00\x00\x00\x01\x00\x01&3\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x08\x08\x00\x00\x00\x00\x00\x01\x00\x01#\xe3\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x088\x00\x00\x00\x00\x00\x01\x00\x01$\xf8\ +\x00\x00\x01\x86\xe4%hu\ +\x00\x00\x08\x1e\x00\x00\x00\x00\x00\x01\x00\x01$y\ +\x00\x00\x01\x86\xe4%hu\ " def qInitResources(): diff --git a/examples/remoteobjects/modelview/doc/modelview.png b/examples/remoteobjects/modelview/doc/modelview.png new file mode 100644 index 00000000..afa275c3 Binary files /dev/null and b/examples/remoteobjects/modelview/doc/modelview.png differ diff --git a/examples/samplebinding/CMakeLists.txt b/examples/samplebinding/CMakeLists.txt index 4807904c..3f355d36 100644 --- a/examples/samplebinding/CMakeLists.txt +++ b/examples/samplebinding/CMakeLists.txt @@ -42,63 +42,24 @@ set(generated_sources ${CMAKE_CURRENT_BINARY_DIR}/${bindings_library}/truck_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(--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) - -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() - +# ================================== Dependency detection ====================================== + +find_package(Python COMPONENTS Interpreter Development REQUIRED) +# On RHEL and some other distros, Python wheels and site-packages may be installed under 'lib64' +# instead of 'lib'. The FindPython CMake module may set Python_SITELIB to 'lib', which is incorrect +# for these cases. To ensure compatibility, we override Python_SITELIB by querying Python directly. +# This guarantees the correct site-packages path is used regardless of platform or Python build. +execute_process( + COMMAND ${Python_EXECUTABLE} -c + "import site; print(next(p for p in site.getsitepackages() if 'site-packages' in p))" + OUTPUT_VARIABLE Python_SITELIB + OUTPUT_STRIP_TRAILING_WHITESPACE +) +message(STATUS "Python site-packages directory: ${Python_SITELIB}") +list(APPEND CMAKE_PREFIX_PATH + "${Python_SITELIB}/shiboken6_generator/lib/cmake" +) +find_package(Shiboken6Tools REQUIRED) # ==================================== RPATH configuration ==================================== @@ -110,7 +71,7 @@ endif() # 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 ${CMAKE_CURRENT_SOURCE_DIR}) set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) # ============================================================================================= # !!! End of dubious section. @@ -129,76 +90,17 @@ set_property(TARGET ${sample_library} PROPERTY PREFIX "") # library can't link to the sample library. target_compile_definitions(${sample_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-return-value-heuristic --use-isnull-as-nb-bool - --avoid-protected-hack - -I${CMAKE_SOURCE_DIR} - -T${CMAKE_SOURCE_DIR} - --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} MODULE ${${bindings_library}_sources}) - -# Apply relevant include and link flags. -target_include_directories(${bindings_library} PRIVATE ${python_include_dir}) -target_include_directories(${bindings_library} PRIVATE ${shiboken_include_dir}) -target_include_directories(${bindings_library} PRIVATE ${CMAKE_SOURCE_DIR}) - -target_link_libraries(${bindings_library} PRIVATE ${shiboken_shared_libraries}) -target_link_libraries(${bindings_library} PRIVATE ${sample_library}) - -# 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() - - +# Create Python bindings using Shiboken6Tools macro +shiboken_generator_create_binding( + EXTENSION_TARGET ${bindings_library} + GENERATED_SOURCES ${generated_sources} + HEADERS ${wrapped_header} + TYPESYSTEM_FILE ${typesystem_file} + LIBRARY_TARGET ${sample_library} + FORCE_LIMITED_API +) # ================================= Dubious deployment section ================================ set(windows_shiboken_shared_libraries) @@ -224,13 +126,14 @@ if(WIN32) 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 PySide6 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() + # Get the correct DLL path for the current build type + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + get_target_property(dll_path Shiboken6::libshiboken IMPORTED_LOCATION_DEBUG) + else() + get_target_property(dll_path Shiboken6::libshiboken IMPORTED_LOCATION_RELEASE) + endif() + file(TO_CMAKE_PATH "${dll_path}" dll_path) + set(windows_shiboken_shared_libraries "${dll_path}") # ========================================================================================= # !!! End of dubious section. # ========================================================================================= @@ -250,3 +153,4 @@ install(FILES ${windows_shiboken_shared_libraries} DESTINATION ${CMAKE_CURRENT_S # ============================================================================================= # !!! End of dubious section. # ============================================================================================= + diff --git a/examples/samplebinding/doc/samplebinding.pyproject b/examples/samplebinding/doc/samplebinding.pyproject index 883c74c0..b0786355 100644 --- a/examples/samplebinding/doc/samplebinding.pyproject +++ b/examples/samplebinding/doc/samplebinding.pyproject @@ -7,6 +7,5 @@ "../main.py", "../truck.cpp", "../truck.h", - "../CMakeLists.txt", - "../../utils/pyside_config.py"] + "../CMakeLists.txt"] } diff --git a/examples/samplebinding/doc/samplebinding.rst b/examples/samplebinding/doc/samplebinding.rst index 930405c5..42c4529e 100644 --- a/examples/samplebinding/doc/samplebinding.rst +++ b/examples/samplebinding/doc/samplebinding.rst @@ -156,8 +156,9 @@ For Windows you will also need: configuration is the same (all Release, which is more likely, or all Debug). -The build uses the ``pyside_config.py`` file to configure the project -using the current PySide/Shiboken installation. +The build uses the ``Shiboken6``, ``Shiboken6Tools``, and ``PySide6`` +CMake packages to configure the project with the current PySide/Shiboken +installation. Using CMake =========== diff --git a/examples/scriptableapplication/CMakeLists.txt b/examples/scriptableapplication/CMakeLists.txt index fbfa00b9..18bb17c4 100644 --- a/examples/scriptableapplication/CMakeLists.txt +++ b/examples/scriptableapplication/CMakeLists.txt @@ -65,13 +65,24 @@ pyside_config(--shiboken-generator-path SHIBOKEN_GENERATOR_PATH) pyside_config(--pyside-path PYSIDE_PATH) pyside_config(--python-include-path PYTHON_INCLUDE_DIR) -pyside_config(--shiboken-generator-include-path SHIBOKEN_GENERATOR_INCLUDE_DIR 1) +pyside_config(--shiboken-include-path SHIBOKEN_INCLUDE_DIR 1) pyside_config(--pyside-include-path PYSIDE_INCLUDE_DIR 1) pyside_config(--python-link-flags-cmake PYTHON_LINKING_DATA 0) pyside_config(--shiboken-module-shared-libraries-cmake SHIBOKEN_MODULE_SHARED_LIBRARIES 0) pyside_config(--pyside-shared-libraries-cmake PYSIDE_SHARED_LIBRARIES 0) +# Print the computed variables +message(STATUS "Shiboken module path: ${SHIBOKEN_MODULE_PATH}") +message(STATUS "Shiboken generator path: ${SHIBOKEN_GENERATOR_PATH}") +message(STATUS "PySide path: ${PYSIDE_PATH}") +message(STATUS "Python include path: ${PYTHON_INCLUDE_DIR}") +message(STATUS "Shiboken include path: ${SHIBOKEN_INCLUDE_DIR}") +message(STATUS "PySide include path: ${PYSIDE_INCLUDE_DIR}") +message(STATUS "Python linking data: ${PYTHON_LINKING_DATA}") +message(STATUS "Shiboken module shared libraries: ${SHIBOKEN_MODULE_SHARED_LIBRARIES}") +message(STATUS "PySide shared libraries: ${PYSIDE_SHARED_LIBRARIES}") + set(SHIBOKEN_PATH "${SHIBOKEN_GENERATOR_PATH}/shiboken6${CMAKE_EXECUTABLE_SUFFIX}") if(NOT EXISTS ${SHIBOKEN_PATH}) @@ -167,7 +178,7 @@ target_sources(${PROJECT_NAME} PUBLIC ${SOURCES}) # Apply relevant include and link flags. target_include_directories(${PROJECT_NAME} PRIVATE ${PYTHON_INCLUDE_DIR}) -target_include_directories(${PROJECT_NAME} PRIVATE ${SHIBOKEN_GENERATOR_INCLUDE_DIR}) +target_include_directories(${PROJECT_NAME} PRIVATE ${SHIBOKEN_INCLUDE_DIR}) target_include_directories(${PROJECT_NAME} PRIVATE ${PYSIDE_INCLUDE_DIR}) target_include_directories(${PROJECT_NAME} PRIVATE ${PYSIDE_ADDITIONAL_INCLUDES}) target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}) diff --git a/examples/scriptableapplication/pythonutils.cpp b/examples/scriptableapplication/pythonutils.cpp index 8104bb16..f726353f 100644 --- a/examples/scriptableapplication/pythonutils.cpp +++ b/examples/scriptableapplication/pythonutils.cpp @@ -69,9 +69,10 @@ State init() Py_Initialize(); qAddPostRoutine(cleanup); state = PythonInitialized; - const bool pythonInitialized = PyInit_AppLib() != nullptr; + auto *appLibModule = PyImport_ImportModule("AppLib"); const bool pyErrorOccurred = PyErr_Occurred() != nullptr; - if (pythonInitialized && !pyErrorOccurred) { + if (appLibModule != nullptr && !pyErrorOccurred) { + Py_DECREF(appLibModule); state = AppModuleLoaded; } else { if (pyErrorOccurred) diff --git a/examples/tutorials/drumpad/final_project/.gitignore b/examples/tutorials/drumpad/final_project/.gitignore new file mode 100644 index 00000000..855f31da --- /dev/null +++ b/examples/tutorials/drumpad/final_project/.gitignore @@ -0,0 +1,11 @@ +__pycache__/ +.DS_Store +build/ +deployment/ +pysidedeploy.spec +resources.py +*.autosave +*.dist/ +Dependencies/ +*.qtds +.qmlls.ini diff --git a/examples/tutorials/drumpad/final_project/Drumpad.qmlproject b/examples/tutorials/drumpad/final_project/Drumpad.qmlproject new file mode 100644 index 00000000..d3105e3c --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad.qmlproject @@ -0,0 +1,69 @@ +// prop: json-converted +// prop: auto-generated + +import QmlProject + +Project { + mainFile: "DrumpadContent/App.qml" + mainUiFile: "DrumpadContent/MainScreen.qml" + targetDirectory: "/opt/Drumpad" + enableCMakeGeneration: false + enablePythonGeneration: true + widgetApp: true + importPaths: [ "." ] + mockImports: [ "Mocks" ] + + qdsVersion: "4.5" + quickVersion: "6.7" + qt6Project: true + qtForMCUs: false + + multilanguageSupport: true + primaryLanguage: "en" + supportedLanguages: [ "en" ] + + Environment { + QML_COMPAT_RESOLVE_URLS_ON_ASSIGNMENT: "1" + QT_AUTO_SCREEN_SCALE_FACTOR: "1" + QT_ENABLE_HIGHDPI_SCALING: "0" + QT_LOGGING_RULES: "qt.qml.connections=false" + QT_QUICK_CONTROLS_CONF: "qtquickcontrols2.conf" + } + + QmlFiles { + directory: "Drumpad" + } + + QmlFiles { + directory: "DrumpadContent" + } + + QmlFiles { + directory: "Generated" + } + + Files { + directory: "Sounds" + filter: "*.mp3;*.wav" + } + + QmlFiles { + directory: "Mocks/Audio" + } + + Files { + files: [ + "qtquickcontrols2.conf" + ] + } + + Files { + directory: "Drumpad" + filter: "qmldir" + } + + Files { + directory: "DrumpadContent" + filter: "*.ttf;*.otf" + } +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad.qrc b/examples/tutorials/drumpad/final_project/Drumpad.qrc new file mode 100644 index 00000000..67868725 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad.qrc @@ -0,0 +1,23 @@ + + + Drumpad.qmlproject + Drumpad/AvailableSoundsComboBox.qml + Drumpad/CenteredFlow.qml + Drumpad/Constants.qml + Drumpad/PadButton.qml + Drumpad/qmldir + Drumpad/SoundEffectPlayer.qml + Drumpad/StyledSpinBox.qml + Drumpad/VolumeSlider.qml + DrumpadContent/App.qml + DrumpadContent/MainScreen.qml + DrumpadContent/qmldir + qtquickcontrols2.conf + Sounds/Bongo Loop 125bpm.wav + Sounds/Clap.wav + Sounds/Closed Hat.wav + Sounds/Kick Drum.wav + Sounds/Open Hat.wav + Sounds/Sine Bass Ebm.wav + + diff --git a/examples/tutorials/drumpad/final_project/Drumpad/AvailableSoundsComboBox.qml b/examples/tutorials/drumpad/final_project/Drumpad/AvailableSoundsComboBox.qml new file mode 100644 index 00000000..2a3330d0 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/AvailableSoundsComboBox.qml @@ -0,0 +1,111 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import Audio + +ComboBox { + id: root + + property string currentFile: currentText ? `Sounds/${currentText}` : "" + required property int initialIndex + + model: audioFilesModel.getModel() + + background: Rectangle { + border.color: root.pressed ? Constants.primaryColor : Constants.secondaryColor + border.width: root.visualFocus ? 3 : 2 + color: root.pressed ? Constants.secondaryColor : "black" + implicitHeight: 30 + radius: 2 + } + contentItem: Text { + color: "white" + elide: Text.ElideRight + leftPadding: 10 + rightPadding: root.indicator.width + 10 + text: root.displayText + verticalAlignment: Text.AlignVCenter + } + delegate: ItemDelegate { + id: delegate + + required property int index + + highlighted: root.highlightedIndex === index + + background: Rectangle { + color: delegate.highlighted ? Constants.darkGray : "black" + implicitWidth: delegate.contentItem.implicitWidth + width: popup.width + } + contentItem: Text { + anchors.fill: parent + color: delegate.highlighted ? "#ff0000" : "white" + elide: Text.ElideRight + leftPadding: 10 + text: root.model[delegate.index] + verticalAlignment: Text.AlignVCenter + } + } + indicator: Canvas { + id: canvas + + contextType: "2d" + height: 8 + width: 12 + x: root.width - canvas.width - root.rightPadding + y: root.topPadding + (root.availableHeight - canvas.height) / 2 + + onPaint: { + let margin = 2; + context.reset(); + context.lineWidth = 2; + context.strokeStyle = "white"; + context.lineCap = "round"; + context.beginPath(); + context.moveTo(margin, margin); + context.lineTo(width / 2, height - margin); + context.lineTo(width - margin, margin); + context.stroke(); + } + + Connections { + function onPressedChanged() { + canvas.requestPaint(); + } + + target: root + } + } + popup: Popup { + id: popup + + implicitHeight: contentItem.implicitHeight + implicitWidth: 200 + padding: 2 + y: root.height + 2 + + background: Rectangle { + border.color: Constants.primaryColor + border.width: 2 + color: "black" + } + contentItem: ListView { + clip: true + currentIndex: root.highlightedIndex + implicitHeight: Math.min(contentHeight, 200) + model: popup.visible ? root.delegateModel : null + } + } + + Component.onCompleted: { + currentIndex = root.initialIndex % model.length; + } + + AudioFilesModel { + id: audioFilesModel + } +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad/CenteredFlow.qml b/examples/tutorials/drumpad/final_project/Drumpad/CenteredFlow.qml new file mode 100644 index 00000000..44911c1b --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/CenteredFlow.qml @@ -0,0 +1,22 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick + +// A Flow layout that centers its children horizontally +// Note that the implementation adds unnecessary spacing in rows that are not full +Flow { + property int customMargin: (children.length && (children[0].width + spacing <= parentWidth)) + ? (parentWidth - rowWidth) / 2 + padding + : padding + property int parentWidth: parent.width - 2 * padding + property int rowCount: children.length ? parentWidth / (children[0].width + spacing) : 0 + property int rowWidth: children.length + ? rowCount * children[0].width + (rowCount - 1) * spacing + 2 * padding + : 0 + + anchors { + leftMargin: customMargin + rightMargin: customMargin + } +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad/Constants.qml b/examples/tutorials/drumpad/final_project/Drumpad/Constants.qml new file mode 100644 index 00000000..6afab9c8 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/Constants.qml @@ -0,0 +1,12 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +pragma Singleton +import QtQuick + +QtObject { + readonly property string darkGray: "#333333" + readonly property string mediumGray: "#9B9B9B" + readonly property string primaryColor: "#FF0000" + readonly property string secondaryColor: "#8C0000" +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad/PadButton.qml b/examples/tutorials/drumpad/final_project/Drumpad/PadButton.qml new file mode 100644 index 00000000..b9564269 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/PadButton.qml @@ -0,0 +1,110 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQuick.Shapes + +Rectangle { + id: root + + property bool isPlaying: false + property bool isError: false + property bool isLoading: false + property int cornerRadius: 10 + signal pressed() + + color: "transparent" + + Shape { + anchors.fill: parent + + ShapePath { + strokeColor: "black" + strokeWidth: 2 + + fillGradient: RadialGradient { + centerRadius: root.height + centerX: root.width / 2 + centerY: root.height / 2 + focalX: centerX + focalY: centerY + + GradientStop { + position: 0 + color: { + if (isError) + return "black"; + if (isLoading) + return "yellow"; + if (isPlaying) + return Qt.darker(Constants.primaryColor, 1.25); + return Qt.darker(Constants.secondaryColor, 1.25); + } + } + GradientStop { + position: 0.5 + color: { + if (isError) + return Constants.darkGray; + if (isLoading) + return "orange"; + if (isPlaying) + return Constants.primaryColor; + return Constants.secondaryColor; + } + } + } + + // Rounded shape path + PathMove { + x: root.cornerRadius + y: 0 + } + PathQuad { + controlX: 0 + controlY: 0 + x: 0 + y: root.cornerRadius + } + PathLine { + x: 0 + y: root.height - root.cornerRadius + } + PathQuad { + controlX: 0 + controlY: root.height + x: root.cornerRadius + y: root.height + } + PathLine { + x: root.width - root.cornerRadius + y: root.height + } + PathQuad { + controlX: root.width + controlY: root.height + x: root.width + y: root.height - root.cornerRadius + } + PathLine { + x: root.width + y: root.cornerRadius + } + PathQuad { + controlX: root.width + controlY: 0 + x: root.width - root.cornerRadius + y: 0 + } + PathLine { + x: root.cornerRadius + y: 0 + } + } + } + + MouseArea { + anchors.fill: parent + onClicked: root.pressed() + } +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad/SoundEffectPlayer.qml b/examples/tutorials/drumpad/final_project/Drumpad/SoundEffectPlayer.qml new file mode 100644 index 00000000..a50b3306 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/SoundEffectPlayer.qml @@ -0,0 +1,118 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQuick.Layouts +import QtQuick.Dialogs +import QtMultimedia + +import Drumpad +import Audio + +Rectangle { + id: root + + property string decodingError: "" + required property int index + property int status: SoundEffect.Null + property bool isLoading: status == SoundEffect.Loading + property bool isError: status == SoundEffect.Error || status == SoundEffect.Null + property bool isReady: status == SoundEffect.Ready + + function play() { + if (root.status == SoundEffect.Ready) { + audioEngine.play(); + } + } + + color: Constants.darkGray + implicitHeight: layout.implicitHeight + 2 * layout.anchors.margins + implicitWidth: layout.implicitWidth + 2 * layout.anchors.margins + radius: 10 + + onDecodingErrorChanged: { + if (status == SoundEffect.Error && root.decodingError) { + errorMessageDialog.text = root.decodingError; + errorMessageDialog.open(); + } + } + + AudioEngine { + id: audioEngine + + file: availableSoundsComboBox.currentFile + volume: volumeSlider.value + + onDecodingStatusChanged: (status, error) => { + root.status = status; + if (status == SoundEffect.Error && error) { + root.decodingError = error; + } else { + root.decodingError = ""; + } + } + } + + MessageDialog { + id: errorMessageDialog + + buttons: MessageDialog.Ok + title: "Error decoding file" + } + + ColumnLayout { + id: layout + + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + RowLayout { + spacing: 10 + + Text { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + color: "white" + text: `Player ${root.index + 1}` + } + AvailableSoundsComboBox { + id: availableSoundsComboBox + + Layout.alignment: Qt.AlignCenter + initialIndex: root.index + } + } + + WaveformItem { + id: waveformItem + + file: audioEngine.file + height: 100 + width: 300 + } + + Row { + Layout.alignment: Qt.AlignCenter + spacing: 10 + + PadButton { + id: padRectangle + height: 100 + width: 100 + isPlaying: audioEngine.isPlaying + isError: root.isError + isLoading: root.isLoading + onPressed: root.play() + } + + VolumeSlider { + id: volumeSlider + + height: padRectangle.height + value: 0.75 + width: 16 + } + } + } +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad/StyledSpinBox.qml b/examples/tutorials/drumpad/final_project/Drumpad/StyledSpinBox.qml new file mode 100644 index 00000000..de95412b --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/StyledSpinBox.qml @@ -0,0 +1,68 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls + +SpinBox { + id: root + + property int innerPadding: 10 + + height: contentItem.implicitHeight + innerPadding + width: contentItem.width + up.indicator.implicitWidth + down.indicator.implicitWidth + + background: Rectangle { + border.color: Constants.secondaryColor + } + + contentItem: Text { + color: "black" + height: parent.height + horizontalAlignment: Text.AlignHCenter + text: root.textFromValue(root.value, root.locale) + verticalAlignment: Text.AlignVCenter + width: implicitWidth + innerPadding * 2 + } + + down.indicator: Rectangle { + border.color: Constants.secondaryColor + color: root.down.pressed ? Constants.mediumGray : enabled ? Constants.darkGray : "black" + height: parent.height + implicitWidth: downText.implicitWidth + innerPadding * 2 + x: root.mirrored ? parent.width - width : 0 + + Text { + id: downText + + anchors.fill: parent + color: "white" + font.pixelSize: Math.round(root.font.pixelSize * 1.5) + fontSizeMode: Text.Fit + horizontalAlignment: Text.AlignHCenter + text: "-" + verticalAlignment: Text.AlignVCenter + } + } + + up.indicator: Rectangle { + border.color: Constants.secondaryColor + color: root.up.pressed ? Constants.mediumGray : enabled ? Constants.darkGray : "black" + height: parent.height + implicitWidth: upText.implicitWidth + innerPadding * 2 + x: root.mirrored ? 0 : parent.width - width + + Text { + id: upText + + anchors.centerIn: parent + anchors.fill: parent + color: "white" + font.pixelSize: Math.round(root.font.pixelSize * 1.5) + fontSizeMode: Text.Fit + horizontalAlignment: Text.AlignHCenter + text: "+" + verticalAlignment: Text.AlignVCenter + } + } +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad/VolumeSlider.qml b/examples/tutorials/drumpad/final_project/Drumpad/VolumeSlider.qml new file mode 100644 index 00000000..10229121 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/VolumeSlider.qml @@ -0,0 +1,39 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls + +Slider { + id: root + + orientation: Qt.Vertical + padding: 0 + + background: Rectangle { + color: Constants.mediumGray + implicitHeight: root.height + implicitWidth: root.width + radius: width / 2 + + Rectangle { + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + color: Qt.lighter(Constants.primaryColor, 1 - (root.visualPosition * 0.3)) + height: (1 - root.visualPosition) * parent.height + (root.visualPosition * handle.height) + radius: parent.width / 2 + width: parent.width + } + } + + handle: Rectangle { + border.color: "#b0b0b0" + border.width: 1 + color: root.pressed ? "#e0e0e0" : "#ffffff" + height: root.width + radius: width / 2 + width: root.width + x: root.availableWidth / 2 - height / 2 + y: root.visualPosition * (root.availableHeight - height) + } +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad/qmldir b/examples/tutorials/drumpad/final_project/Drumpad/qmldir new file mode 100644 index 00000000..7dba78c1 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/qmldir @@ -0,0 +1,10 @@ +module Drumpad + +AvailableSoundsComboBox 1.0 AvailableSoundsComboBox.qml +SoundEffectPlayer 1.0 SoundEffectPlayer.qml +CenteredFlow 1.0 CenteredFlow.qml +VolumeSlider 1.0 VolumeSlider.qml +StyledSpinBox 1.0 StyledSpinBox.qml +PadButton 1.0 PadButton.qml + +singleton Constants 1.0 Constants.qml diff --git a/examples/tutorials/drumpad/final_project/DrumpadContent/App.qml b/examples/tutorials/drumpad/final_project/DrumpadContent/App.qml new file mode 100644 index 00000000..773cbbfc --- /dev/null +++ b/examples/tutorials/drumpad/final_project/DrumpadContent/App.qml @@ -0,0 +1,21 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import Drumpad 1.0 + +Window { + id: root + + height: 800 + title: "Drumpad" + visible: true + width: 1200 + + MainScreen { + id: mainScreen + + anchors.fill: parent + } +} diff --git a/examples/tutorials/drumpad/final_project/DrumpadContent/MainScreen.qml b/examples/tutorials/drumpad/final_project/DrumpadContent/MainScreen.qml new file mode 100644 index 00000000..fdbd7b66 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/DrumpadContent/MainScreen.qml @@ -0,0 +1,99 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Drumpad +import Audio + +Rectangle { + id: root + + property QtObject soundEffectPlayer: Qt.createComponent("../Drumpad/SoundEffectPlayer.qml", + Component.PreferSynchronous) + + color: "black" + focus: true + + Component.onCompleted: { + // Initialize the default sound effect players + for (var i = 0; i < audioPlayersSpinBox.value; i++) { + root.soundEffectPlayer.createObject(soundEffectPlayersFlow, { + index: i + }); + } + } + Keys.onPressed: event => { + if (event.key < Qt.Key_1 || event.key > Qt.Key_9) { + // Ignore key out of scope + return; + } + + let digit = event.key - Qt.Key_1; + if (digit < soundEffectPlayersFlow.children.length) { + soundEffectPlayersFlow.children[digit].play(); + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + + Row { + id: audioPlayersCountRow + + Layout.alignment: Qt.AlignHCenter + spacing: 5 + + Text { + anchors.verticalCenter: parent.verticalCenter + color: "white" + text: "Audio players:" + } + + StyledSpinBox { + id: audioPlayersSpinBox + + value: 5 + + onValueModified: { + let soundPlayersCount = soundEffectPlayersFlow.children.length; + if (audioPlayersSpinBox.value < soundPlayersCount) { + // Remove extra sound effect players + soundEffectPlayersFlow.children.length = audioPlayersSpinBox.value; + return; + } + + if (audioPlayersSpinBox.value < soundPlayersCount) { + return; + } + // Create more sound effect players + for (var i = soundPlayersCount; i < audioPlayersSpinBox.value; i++) { + root.soundEffectPlayer.createObject(soundEffectPlayersFlow, { + index: i + }); + } + } + } + } + + ScrollView { + Layout.fillHeight: true + Layout.fillWidth: true + contentWidth: width + + background: Rectangle { + color: "#232323" + } + + CenteredFlow { + id: soundEffectPlayersFlow + + anchors.fill: parent + padding: 10 + spacing: 10 + } + } + } +} diff --git a/examples/tutorials/drumpad/final_project/DrumpadContent/qmldir b/examples/tutorials/drumpad/final_project/DrumpadContent/qmldir new file mode 100644 index 00000000..f1f34c52 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/DrumpadContent/qmldir @@ -0,0 +1,4 @@ +module DrumpadContent + +App 1.0 App.qml +MainScreen 1.0 MainScreen.qml diff --git a/examples/tutorials/drumpad/final_project/Mocks/Audio/AudioEngine.qml b/examples/tutorials/drumpad/final_project/Mocks/Audio/AudioEngine.qml new file mode 100644 index 00000000..4bfbc24f --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Mocks/Audio/AudioEngine.qml @@ -0,0 +1,27 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtMultimedia + +Item { + id: root + + property double volume + property url file + + MediaPlayer { + id: player + source: file + audioOutput: AudioOutput {} + } + + onVolumeChanged : { + console.log("Mock: VolumeChanaged ", volume ) + } + + function play() { + console.log("Mock: play()") + player.play() + } +} diff --git a/examples/tutorials/drumpad/final_project/Mocks/Audio/WaveformItem.qml b/examples/tutorials/drumpad/final_project/Mocks/Audio/WaveformItem.qml new file mode 100644 index 00000000..fcb6041b --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Mocks/Audio/WaveformItem.qml @@ -0,0 +1,13 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +Rectangle { + id: root + width: 1920 + height: 1080 + color: "blue" + property url file +} diff --git a/examples/tutorials/drumpad/final_project/Mocks/Audio/qmldir b/examples/tutorials/drumpad/final_project/Mocks/Audio/qmldir new file mode 100644 index 00000000..189d68e6 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Mocks/Audio/qmldir @@ -0,0 +1,3 @@ +module Audio +AudioEngine 1.0 AudioEngine.qml +WaveformItem 1.0 WaveformItem.qml diff --git a/examples/tutorials/drumpad/final_project/Mocks/Components/AudioFilesModel.qml b/examples/tutorials/drumpad/final_project/Mocks/Components/AudioFilesModel.qml new file mode 100644 index 00000000..7e4ff22a --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Mocks/Components/AudioFilesModel.qml @@ -0,0 +1,8 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +Item { + getFiles: function() { + console.log("AudioFilesModel mock: getFiles()") + } +} diff --git a/examples/tutorials/drumpad/final_project/Mocks/Components/qmldir b/examples/tutorials/drumpad/final_project/Mocks/Components/qmldir new file mode 100644 index 00000000..2d8e9899 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Mocks/Components/qmldir @@ -0,0 +1,2 @@ +module Components +AudioFilesModel 1.0 AudioFilesModel.qml diff --git a/examples/tutorials/drumpad/final_project/Python/audio/__init__.py b/examples/tutorials/drumpad/final_project/Python/audio/__init__.py new file mode 100644 index 00000000..817bc3e1 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Python/audio/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from .audio_engine import AudioEngine +from .waveform_item import WaveformItem +from .audio_files_model import AudioFilesModel diff --git a/examples/tutorials/drumpad/final_project/Python/audio/audio_engine.py b/examples/tutorials/drumpad/final_project/Python/audio/audio_engine.py new file mode 100644 index 00000000..daf2b356 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Python/audio/audio_engine.py @@ -0,0 +1,65 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtQml import QmlElement +from PySide6.QtCore import QObject, Slot, Property, Signal, QUrl +from PySide6.QtMultimedia import QSoundEffect + +from autogen.settings import project_root + +QML_IMPORT_NAME = "Audio" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +class AudioEngine(QObject): + volumeChanged = Signal() + fileChanged = Signal() + isPlayingChanged = Signal() + decodingStatusChanged = Signal(QSoundEffect.Status, str) + + def __init__(self, parent=None): + super().__init__(parent) + self._sound_effect = QSoundEffect() + self._sound_effect.playingChanged.connect(self.isPlayingChanged.emit) # + self._sound_effect.statusChanged.connect(self.reportStatus) + + def reportStatus(self): + if self._sound_effect.status() == QSoundEffect.Status.Error: + self.decodingStatusChanged.emit( + QSoundEffect.Status.Error, + f"Error decoding file: {self._sound_effect.source().path()}", + ) + else: + self.decodingStatusChanged.emit(self._sound_effect.status(), "") + + @Slot(result=None) + def play(self): + self._sound_effect.play() + + def volume(self): + return self._sound_effect.volume() + + def setVolume(self, value): + self._sound_effect.setVolume(value) + self.volumeChanged.emit() + + def file(self): + return self._sound_effect.source() + + def setFile(self, value: QUrl): + if self._sound_effect.source() == value or value.isEmpty(): + return + + if "__compiled__" in globals(): + self._sound_effect.setSource(f"qrc:/{value.toString()}") + else: + self._sound_effect.setSource(f"file:{project_root / value.toString()}") + self.fileChanged.emit() + + def isPlaying(self): + return self._sound_effect.isPlaying() + + volume = Property(float, volume, setVolume, notify=volumeChanged) + file = Property(QUrl, file, setFile, notify=fileChanged) + isPlaying = Property(bool, isPlaying, notify=isPlayingChanged) diff --git a/examples/tutorials/drumpad/final_project/Python/audio/audio_files_model.py b/examples/tutorials/drumpad/final_project/Python/audio/audio_files_model.py new file mode 100644 index 00000000..b92bc247 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Python/audio/audio_files_model.py @@ -0,0 +1,29 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from pathlib import Path + +from PySide6.QtCore import QObject, Slot, QDirIterator +from PySide6.QtQml import QmlElement + +from autogen.settings import project_root + + +QML_IMPORT_NAME = "Audio" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +class AudioFilesModel(QObject): + @Slot(result=list) + def getModel(self): + if "__compiled__" in globals(): + resource_prefix = ":/Sounds/" + iterator = QDirIterator(resource_prefix, QDirIterator.Subdirectories) + audio_files = [] + while iterator.hasNext(): + resource = iterator.next() + audio_files.append(resource.split(resource_prefix)[-1]) + return audio_files + + return list(p.name for p in Path(project_root / "Sounds").glob("*.wav")) diff --git a/examples/tutorials/drumpad/final_project/Python/audio/waveform_item.py b/examples/tutorials/drumpad/final_project/Python/audio/waveform_item.py new file mode 100644 index 00000000..d3ce0f43 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Python/audio/waveform_item.py @@ -0,0 +1,113 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import struct + +from PySide6.QtCore import Qt, Property, QUrl, Signal, QFile, QPointF +from PySide6.QtGui import QPen, QPainter +from PySide6.QtMultimedia import QAudioFormat, QAudioDecoder +from PySide6.QtQml import QmlElement +from PySide6.QtQuick import QQuickPaintedItem + +QML_IMPORT_NAME = "Audio" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +class WaveformItem(QQuickPaintedItem): + + fileChanged = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self._waveformData = [] + self._background_color = Qt.black + + audio_format = QAudioFormat() + audio_format.setChannelCount(1) + audio_format.setSampleRate(44100) + audio_format.setSampleFormat(QAudioFormat.Float) + + self._file_url: QUrl | None = None + self._audio_file: QFile | None = None + + self._decoder = QAudioDecoder() + self._decoder.setAudioFormat(audio_format) + + self._decoder.bufferReady.connect(self.onBufferReady) + self._decoder.finished.connect(self.decoderFinished) + + def file(self) -> QUrl | None: + return self._file_url + + def setFile(self, value: QUrl): + if self._decoder.source() == value: + return + + if self._audio_file and self._audio_file.isOpen(): + self._audio_file.close() + + self._waveformData = [] + self._decoder.stop() + + self._file_url = value + if "__compiled__" in globals(): + path = self._file_url.toString().replace("qrc:/", ":/") + else: + path = self._file_url.path() + self._audio_file = QFile(path) + self._audio_file.open(QFile.ReadOnly) + self._decoder.setSourceDevice(self._audio_file) + self._decoder.start() + self.fileChanged.emit() + + def paint(self, painter): + # Fill the bounding rectangle with the specified color + painter.fillRect(self.boundingRect(), self._background_color) + + # If no waveform data is available, draw the text + if not self._waveformData: + painter.setPen(Qt.white) + painter.drawText(self.boundingRect(), Qt.AlignCenter, "Waveform not available") + return + + painter.setRenderHint(QPainter.Antialiasing) + + # Set the pen for drawing the waveform + pen = QPen(Qt.blue) + pen.setWidth(1) + painter.setPen(pen) + + # Get container dimensions + rect = self.boundingRect() + data_size = len(self._waveformData) + + # Calculate step size and center line + x_step = rect.width() / data_size + center_y = rect.height() / 2.0 + + # Draw the waveform as connected lines + for i in range(1, data_size): + x1 = (i - 1) * x_step + y1 = center_y - self._waveformData[i - 1] * center_y + x2 = i * x_step + y2 = center_y - self._waveformData[i] * center_y + painter.drawLine(QPointF(x1, y1), QPointF(x2, y2)) + + @staticmethod + def float_buffer_to_list(data): + # Calculate the number of 32-bit floats in the buffer + float_count = len(data) // 4 # Each float32 is 4 bytes + # Unpack the binary data into a list of floats + return list(struct.unpack(f"{float_count}f", data)) + + def onBufferReady(self): + buffer = self._decoder.read() + data = buffer.constData() + self._waveformData.extend(self.float_buffer_to_list(data)) + self.update() + + file: QUrl = Property(QUrl, file, setFile, notify=fileChanged) + + def decoderFinished(self): + self._audio_file.close() diff --git a/examples/tutorials/drumpad/final_project/Python/autogen/settings.py b/examples/tutorials/drumpad/final_project/Python/autogen/settings.py new file mode 100644 index 00000000..39386a27 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Python/autogen/settings.py @@ -0,0 +1,39 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +# This file is automatically generated by Qt Design Studio. +import os +import sys +from pathlib import Path + +from PySide6.QtQml import QQmlApplicationEngine + +project_root = Path(__file__).parent.parent.parent + + +def setup_qt_environment(qml_engine: QQmlApplicationEngine): + """ + Load the QML application. Import the compiled resources when the application is deployed. + """ + qml_app_url = "DrumpadContent/App.qml" + + if "__compiled__" in globals(): + # Application has been deployed using pyside6-deploy + try: + import autogen.resources # noqa: F401 + except ImportError: + resource_file = Path(__file__).parent / "resources.py" + print( + f"Error: No compiled resources found in {resource_file.absolute()}\n" + f"Please compile the resources using pyside6-rcc or pyside6-project build", + file=sys.stderr, + ) + sys.exit(1) + + qml_engine.addImportPath(":/") + qml_engine.load(f":/{qml_app_url}") + return + + qml_engine.addImportPath(str(project_root.absolute())) + os.environ["QT_QUICK_CONTROLS_CONF"] = str(project_root / "qtquickcontrols2.conf") + qml_engine.load(str(project_root / qml_app_url)) diff --git a/examples/tutorials/drumpad/final_project/Python/main.py b/examples/tutorials/drumpad/final_project/Python/main.py new file mode 100644 index 00000000..166f2514 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Python/main.py @@ -0,0 +1,28 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtGui import QGuiApplication +from PySide6.QtQml import QQmlApplicationEngine + +from autogen.settings import setup_qt_environment +from audio import * # noqa: F401,F403 + + +def main(): + app = QGuiApplication(sys.argv) + engine = QQmlApplicationEngine() + + setup_qt_environment(engine) + + if not engine.rootObjects(): + sys.exit(-1) + + ex = app.exec() + del engine + return ex + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/tutorials/drumpad/final_project/Python/pyproject.toml b/examples/tutorials/drumpad/final_project/Python/pyproject.toml new file mode 100644 index 00000000..fcb5bbb9 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Python/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "Drumpad" + +[tool.pyside6-project] +files = ["main.py", "autogen/settings.py", "audio/audio_files_model.py", "audio/audio_engine.py", "audio/waveform_item.py", "../Drumpad.qmlproject", "../Drumpad.qrc", "../qtquickcontrols2.conf", "../Drumpad/AvailableSoundsComboBox.qml", "../Drumpad/CenteredFlow.qml", "../Drumpad/Constants.qml", "../Drumpad/PadButton.qml", "../Drumpad/qmldir", "../Drumpad/SoundEffectPlayer.qml", "../Drumpad/StyledSpinBox.qml", "../Drumpad/VolumeSlider.qml", "../DrumpadContent/App.qml", "../DrumpadContent/MainScreen.qml", "../DrumpadContent/qmldir", "../Mocks/Audio/AudioEngine.qml", "../Mocks/Audio/qmldir", "../Mocks/Audio/WaveformItem.qml", "../Mocks/Components/AudioFilesModel.qml", "../Mocks/Components/qmldir"] diff --git a/examples/tutorials/drumpad/final_project/Sounds/Bongo Loop 125bpm.wav b/examples/tutorials/drumpad/final_project/Sounds/Bongo Loop 125bpm.wav new file mode 100644 index 00000000..b90bc45e Binary files /dev/null and b/examples/tutorials/drumpad/final_project/Sounds/Bongo Loop 125bpm.wav differ diff --git a/examples/tutorials/drumpad/final_project/Sounds/Clap.wav b/examples/tutorials/drumpad/final_project/Sounds/Clap.wav new file mode 100644 index 00000000..aceee331 Binary files /dev/null and b/examples/tutorials/drumpad/final_project/Sounds/Clap.wav differ diff --git a/examples/tutorials/drumpad/final_project/Sounds/Closed Hat.wav b/examples/tutorials/drumpad/final_project/Sounds/Closed Hat.wav new file mode 100644 index 00000000..d062e723 Binary files /dev/null and b/examples/tutorials/drumpad/final_project/Sounds/Closed Hat.wav differ diff --git a/examples/tutorials/drumpad/final_project/Sounds/Kick Drum.wav b/examples/tutorials/drumpad/final_project/Sounds/Kick Drum.wav new file mode 100644 index 00000000..e2833713 Binary files /dev/null and b/examples/tutorials/drumpad/final_project/Sounds/Kick Drum.wav differ diff --git a/examples/tutorials/drumpad/final_project/Sounds/Open Hat.wav b/examples/tutorials/drumpad/final_project/Sounds/Open Hat.wav new file mode 100644 index 00000000..e6fcb130 Binary files /dev/null and b/examples/tutorials/drumpad/final_project/Sounds/Open Hat.wav differ diff --git a/examples/tutorials/drumpad/final_project/Sounds/Sine Bass Ebm.wav b/examples/tutorials/drumpad/final_project/Sounds/Sine Bass Ebm.wav new file mode 100644 index 00000000..5925d0fc Binary files /dev/null and b/examples/tutorials/drumpad/final_project/Sounds/Sine Bass Ebm.wav differ diff --git a/examples/tutorials/drumpad/final_project/doc/final_project.md b/examples/tutorials/drumpad/final_project/doc/final_project.md new file mode 100644 index 00000000..877bc017 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/doc/final_project.md @@ -0,0 +1,12 @@ +# Drumpad example (Qt Design Studio) - Final project + +This example contains the final [Qt Design Studio] project of the [Qt Design Studio integration tutorial]. +It contains all the necessary files to execute the project, including the Python code developed +along the tutorial. + +For more details, see the [Qt Design Studio integration tutorial]. + +To download the initial project source code, visit {ref}`example_tutorials_drumpad_initial_project`. + +[Qt Design Studio]: https://www.qt.io/product/ui-design-tools/ +[Qt Design Studio integration tutorial]: tutorial_qt_design_studio_integration diff --git a/examples/tutorials/drumpad/final_project/qtquickcontrols2.conf b/examples/tutorials/drumpad/final_project/qtquickcontrols2.conf new file mode 100644 index 00000000..87a95d01 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/qtquickcontrols2.conf @@ -0,0 +1,6 @@ +; This file can be edited to change the style of the application +; Read "Qt Quick Controls 2 Configuration File" for details: +; http://doc.qt.io/qt-5/qtquickcontrols2-configuration.html + +[Controls] +Style=Basic diff --git a/examples/tutorials/drumpad/initial_project/.gitignore b/examples/tutorials/drumpad/initial_project/.gitignore new file mode 100644 index 00000000..855f31da --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/.gitignore @@ -0,0 +1,11 @@ +__pycache__/ +.DS_Store +build/ +deployment/ +pysidedeploy.spec +resources.py +*.autosave +*.dist/ +Dependencies/ +*.qtds +.qmlls.ini diff --git a/examples/tutorials/drumpad/initial_project/Drumpad.qmlproject b/examples/tutorials/drumpad/initial_project/Drumpad.qmlproject new file mode 100644 index 00000000..b92c65cb --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad.qmlproject @@ -0,0 +1,69 @@ +// prop: json-converted +// prop: auto-generated + +import QmlProject + +Project { + mainFile: "DrumpadContent/App.qml" + mainUiFile: "DrumpadContent/MainScreen.qml" + targetDirectory: "/opt/Drumpad" + enableCMakeGeneration: false + enablePythonGeneration: false + widgetApp: true + importPaths: [ "." ] + mockImports: [ "Mocks" ] + + qdsVersion: "4.5" + quickVersion: "6.7" + qt6Project: true + qtForMCUs: false + + multilanguageSupport: true + primaryLanguage: "en" + supportedLanguages: [ "en" ] + + Environment { + QML_COMPAT_RESOLVE_URLS_ON_ASSIGNMENT: "1" + QT_AUTO_SCREEN_SCALE_FACTOR: "1" + QT_ENABLE_HIGHDPI_SCALING: "0" + QT_LOGGING_RULES: "qt.qml.connections=false" + QT_QUICK_CONTROLS_CONF: "qtquickcontrols2.conf" + } + + QmlFiles { + directory: "Drumpad" + } + + QmlFiles { + directory: "DrumpadContent" + } + + QmlFiles { + directory: "Generated" + } + + Files { + directory: "Sounds" + filter: "*.mp3;*.wav" + } + + QmlFiles { + directory: "Mocks/Audio" + } + + Files { + files: [ + "qtquickcontrols2.conf" + ] + } + + Files { + directory: "Drumpad" + filter: "qmldir" + } + + Files { + directory: "DrumpadContent" + filter: "*.ttf;*.otf" + } +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad.qrc b/examples/tutorials/drumpad/initial_project/Drumpad.qrc new file mode 100644 index 00000000..7415635b --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad.qrc @@ -0,0 +1,18 @@ + + + Drumpad.qmlproject + Drumpad/AvailableSoundsComboBox.qml + Drumpad/CenteredFlow.qml + Drumpad/Constants.qml + Drumpad/PadButton.qml + Drumpad/qmldir + Drumpad/SoundEffectPlayer.qml + Drumpad/StyledSpinBox.qml + Drumpad/VolumeSlider.qml + DrumpadContent/App.qml + DrumpadContent/MainScreen.qml + DrumpadContent/qmldir + qtquickcontrols2.conf + Sounds/Clap.wav + + diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/AvailableSoundsComboBox.qml b/examples/tutorials/drumpad/initial_project/Drumpad/AvailableSoundsComboBox.qml new file mode 100644 index 00000000..e105e222 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/AvailableSoundsComboBox.qml @@ -0,0 +1,111 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import Audio + +ComboBox { + id: root + + property string currentFile: currentText ? `../Sounds/${currentText}` : "" + required property int initialIndex + + model: audioFilesModel.getModel() + + background: Rectangle { + border.color: root.pressed ? Constants.primaryColor : Constants.secondaryColor + border.width: root.visualFocus ? 3 : 2 + color: root.pressed ? Constants.secondaryColor : "black" + implicitHeight: 30 + radius: 2 + } + contentItem: Text { + color: "white" + elide: Text.ElideRight + leftPadding: 10 + rightPadding: root.indicator.width + 10 + text: root.displayText + verticalAlignment: Text.AlignVCenter + } + delegate: ItemDelegate { + id: delegate + + required property int index + + highlighted: root.highlightedIndex === index + + background: Rectangle { + color: delegate.highlighted ? Constants.darkGray : "black" + implicitWidth: delegate.contentItem.implicitWidth + width: popup.width + } + contentItem: Text { + anchors.fill: parent + color: delegate.highlighted ? "#ff0000" : "white" + elide: Text.ElideRight + leftPadding: 10 + text: root.model[delegate.index] + verticalAlignment: Text.AlignVCenter + } + } + indicator: Canvas { + id: canvas + + contextType: "2d" + height: 8 + width: 12 + x: root.width - canvas.width - root.rightPadding + y: root.topPadding + (root.availableHeight - canvas.height) / 2 + + onPaint: { + let margin = 2; + context.reset(); + context.lineWidth = 2; + context.strokeStyle = "white"; + context.lineCap = "round"; + context.beginPath(); + context.moveTo(margin, margin); + context.lineTo(width / 2, height - margin); + context.lineTo(width - margin, margin); + context.stroke(); + } + + Connections { + function onPressedChanged() { + canvas.requestPaint(); + } + + target: root + } + } + popup: Popup { + id: popup + + implicitHeight: contentItem.implicitHeight + implicitWidth: 200 + padding: 2 + y: root.height + 2 + + background: Rectangle { + border.color: Constants.primaryColor + border.width: 2 + color: "black" + } + contentItem: ListView { + clip: true + currentIndex: root.highlightedIndex + implicitHeight: Math.min(contentHeight, 200) + model: popup.visible ? root.delegateModel : null + } + } + + Component.onCompleted: { + currentIndex = root.initialIndex % model.length; + } + + AudioFilesModel { + id: audioFilesModel + } +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/CenteredFlow.qml b/examples/tutorials/drumpad/initial_project/Drumpad/CenteredFlow.qml new file mode 100644 index 00000000..a5e9fe2c --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/CenteredFlow.qml @@ -0,0 +1,22 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +// A Flow layout that centers its children horizontally +// Note that the implementation adds unnecessary spacing in rows that are not full +Flow { + property int customMargin: (children.length && (children[0].width + spacing <= parentWidth)) + ? (parentWidth - rowWidth) / 2 + padding + : padding + property int parentWidth: parent.width - 2 * padding + property int rowCount: children.length ? parentWidth / (children[0].width + spacing) : 0 + property int rowWidth: children.length + ? rowCount * children[0].width + (rowCount - 1) * spacing + 2 * padding + : 0 + + anchors { + leftMargin: customMargin + rightMargin: customMargin + } +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/Constants.qml b/examples/tutorials/drumpad/initial_project/Drumpad/Constants.qml new file mode 100644 index 00000000..73058229 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/Constants.qml @@ -0,0 +1,12 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma Singleton +import QtQuick + +QtObject { + readonly property string darkGray: "#333333" + readonly property string mediumGray: "#9B9B9B" + readonly property string primaryColor: "#FF0000" + readonly property string secondaryColor: "#8C0000" +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/PadButton.qml b/examples/tutorials/drumpad/initial_project/Drumpad/PadButton.qml new file mode 100644 index 00000000..e00d77db --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/PadButton.qml @@ -0,0 +1,110 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Shapes + +Rectangle { + id: root + + property bool isPlaying: false + property bool isError: false + property bool isLoading: false + property int cornerRadius: 10 + signal pressed() + + color: "transparent" + + Shape { + anchors.fill: parent + + ShapePath { + strokeColor: "black" + strokeWidth: 2 + + fillGradient: RadialGradient { + centerRadius: root.height + centerX: root.width / 2 + centerY: root.height / 2 + focalX: centerX + focalY: centerY + + GradientStop { + position: 0 + color: { + if (isError) + return "black"; + if (isLoading) + return "yellow"; + if (isPlaying) + return Qt.darker(Constants.primaryColor, 1.25); + return Qt.darker(Constants.secondaryColor, 1.25); + } + } + GradientStop { + position: 0.5 + color: { + if (isError) + return Constants.darkGray; + if (isLoading) + return "orange"; + if (isPlaying) + return Constants.primaryColor; + return Constants.secondaryColor; + } + } + } + + // Rounded shape path + PathMove { + x: root.cornerRadius + y: 0 + } + PathQuad { + controlX: 0 + controlY: 0 + x: 0 + y: root.cornerRadius + } + PathLine { + x: 0 + y: root.height - root.cornerRadius + } + PathQuad { + controlX: 0 + controlY: root.height + x: root.cornerRadius + y: root.height + } + PathLine { + x: root.width - root.cornerRadius + y: root.height + } + PathQuad { + controlX: root.width + controlY: root.height + x: root.width + y: root.height - root.cornerRadius + } + PathLine { + x: root.width + y: root.cornerRadius + } + PathQuad { + controlX: root.width + controlY: 0 + x: root.width - root.cornerRadius + y: 0 + } + PathLine { + x: root.cornerRadius + y: 0 + } + } + } + + MouseArea { + anchors.fill: parent + onClicked: root.pressed() + } +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/SoundEffectPlayer.qml b/examples/tutorials/drumpad/initial_project/Drumpad/SoundEffectPlayer.qml new file mode 100644 index 00000000..7232b966 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/SoundEffectPlayer.qml @@ -0,0 +1,118 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Layouts +import QtQuick.Dialogs +import QtMultimedia + +import Drumpad +import Audio + +Rectangle { + id: root + + property string decodingError: "" + required property int index + property int status: SoundEffect.Null + property bool isLoading: status == SoundEffect.Loading + property bool isError: status == SoundEffect.Error || status == SoundEffect.Null + property bool isReady: status == SoundEffect.Ready + + function play() { + if (root.status == SoundEffect.Ready) { + audioEngine.play(); + } + } + + color: Constants.darkGray + implicitHeight: layout.implicitHeight + 2 * layout.anchors.margins + implicitWidth: layout.implicitWidth + 2 * layout.anchors.margins + radius: 10 + + onDecodingErrorChanged: { + if (status == SoundEffect.Error && root.decodingError) { + errorMessageDialog.text = root.decodingError; + errorMessageDialog.open(); + } + } + + AudioEngine { + id: audioEngine + + file: availableSoundsComboBox.currentFile + volume: volumeSlider.value + + onDecodingStatusChanged: (status, error) => { + root.status = status; + if (status == SoundEffect.Error && error) { + root.decodingError = error; + } else { + root.decodingError = ""; + } + } + } + + MessageDialog { + id: errorMessageDialog + + buttons: MessageDialog.Ok + title: "Error decoding file" + } + + ColumnLayout { + id: layout + + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + RowLayout { + spacing: 10 + + Text { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + color: "white" + text: `Player ${root.index + 1}` + } + AvailableSoundsComboBox { + id: availableSoundsComboBox + + Layout.alignment: Qt.AlignCenter + initialIndex: root.index + } + } + + WaveformItem { + id: waveformItem + + file: audioEngine.file + height: 100 + width: 300 + } + + Row { + Layout.alignment: Qt.AlignCenter + spacing: 10 + + PadButton { + id: padRectangle + height: 100 + width: 100 + isPlaying: audioEngine.isPlaying + isError: root.isError + isLoading: root.isLoading + onPressed: root.play() + } + + VolumeSlider { + id: volumeSlider + + height: padRectangle.height + value: 0.75 + width: 16 + } + } + } +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/StyledSpinBox.qml b/examples/tutorials/drumpad/initial_project/Drumpad/StyledSpinBox.qml new file mode 100644 index 00000000..c403be0d --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/StyledSpinBox.qml @@ -0,0 +1,68 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +SpinBox { + id: root + + property int innerPadding: 10 + + height: contentItem.implicitHeight + innerPadding + width: contentItem.width + up.indicator.implicitWidth + down.indicator.implicitWidth + + background: Rectangle { + border.color: Constants.secondaryColor + } + + contentItem: Text { + color: "black" + height: parent.height + horizontalAlignment: Text.AlignHCenter + text: root.textFromValue(root.value, root.locale) + verticalAlignment: Text.AlignVCenter + width: implicitWidth + innerPadding * 2 + } + + down.indicator: Rectangle { + border.color: Constants.secondaryColor + color: root.down.pressed ? Constants.mediumGray : enabled ? Constants.darkGray : "black" + height: parent.height + implicitWidth: downText.implicitWidth + innerPadding * 2 + x: root.mirrored ? parent.width - width : 0 + + Text { + id: downText + + anchors.fill: parent + color: "white" + font.pixelSize: Math.round(root.font.pixelSize * 1.5) + fontSizeMode: Text.Fit + horizontalAlignment: Text.AlignHCenter + text: "-" + verticalAlignment: Text.AlignVCenter + } + } + + up.indicator: Rectangle { + border.color: Constants.secondaryColor + color: root.up.pressed ? Constants.mediumGray : enabled ? Constants.darkGray : "black" + height: parent.height + implicitWidth: upText.implicitWidth + innerPadding * 2 + x: root.mirrored ? 0 : parent.width - width + + Text { + id: upText + + anchors.centerIn: parent + anchors.fill: parent + color: "white" + font.pixelSize: Math.round(root.font.pixelSize * 1.5) + fontSizeMode: Text.Fit + horizontalAlignment: Text.AlignHCenter + text: "+" + verticalAlignment: Text.AlignVCenter + } + } +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/VolumeSlider.qml b/examples/tutorials/drumpad/initial_project/Drumpad/VolumeSlider.qml new file mode 100644 index 00000000..0fd1eea4 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/VolumeSlider.qml @@ -0,0 +1,39 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +Slider { + id: root + + orientation: Qt.Vertical + padding: 0 + + background: Rectangle { + color: Constants.mediumGray + implicitHeight: root.height + implicitWidth: root.width + radius: width / 2 + + Rectangle { + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + color: Qt.lighter(Constants.primaryColor, 1 - (root.visualPosition * 0.3)) + height: (1 - root.visualPosition) * parent.height + (root.visualPosition * handle.height) + radius: parent.width / 2 + width: parent.width + } + } + + handle: Rectangle { + border.color: "#b0b0b0" + border.width: 1 + color: root.pressed ? "#e0e0e0" : "#ffffff" + height: root.width + radius: width / 2 + width: root.width + x: root.availableWidth / 2 - height / 2 + y: root.visualPosition * (root.availableHeight - height) + } +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/qmldir b/examples/tutorials/drumpad/initial_project/Drumpad/qmldir new file mode 100644 index 00000000..7dba78c1 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/qmldir @@ -0,0 +1,10 @@ +module Drumpad + +AvailableSoundsComboBox 1.0 AvailableSoundsComboBox.qml +SoundEffectPlayer 1.0 SoundEffectPlayer.qml +CenteredFlow 1.0 CenteredFlow.qml +VolumeSlider 1.0 VolumeSlider.qml +StyledSpinBox 1.0 StyledSpinBox.qml +PadButton 1.0 PadButton.qml + +singleton Constants 1.0 Constants.qml diff --git a/examples/tutorials/drumpad/initial_project/DrumpadContent/App.qml b/examples/tutorials/drumpad/initial_project/DrumpadContent/App.qml new file mode 100644 index 00000000..e1e4b4d3 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/DrumpadContent/App.qml @@ -0,0 +1,21 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import Drumpad 1.0 + +Window { + id: root + + height: 800 + title: "Drumpad" + visible: true + width: 1200 + + MainScreen { + id: mainScreen + + anchors.fill: parent + } +} diff --git a/examples/tutorials/drumpad/initial_project/DrumpadContent/MainScreen.qml b/examples/tutorials/drumpad/initial_project/DrumpadContent/MainScreen.qml new file mode 100644 index 00000000..2754c4c9 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/DrumpadContent/MainScreen.qml @@ -0,0 +1,99 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Drumpad +import Audio + +Rectangle { + id: root + + property QtObject soundEffectPlayer: Qt.createComponent("../Drumpad/SoundEffectPlayer.qml", + Component.PreferSynchronous) + + color: "black" + focus: true + + Component.onCompleted: { + // Initialize the default sound effect players + for (var i = 0; i < audioPlayersSpinBox.value; i++) { + root.soundEffectPlayer.createObject(soundEffectPlayersFlow, { + index: i + }); + } + } + Keys.onPressed: event => { + if (event.key < Qt.Key_1 || event.key > Qt.Key_9) { + // Ignore key out of scope + return; + } + + let digit = event.key - Qt.Key_1; + if (digit < soundEffectPlayersFlow.children.length) { + soundEffectPlayersFlow.children[digit].play(); + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + + Row { + id: audioPlayersCountRow + + Layout.alignment: Qt.AlignHCenter + spacing: 5 + + Text { + anchors.verticalCenter: parent.verticalCenter + color: "white" + text: "Audio players:" + } + + StyledSpinBox { + id: audioPlayersSpinBox + + value: 5 + + onValueModified: { + let soundPlayersCount = soundEffectPlayersFlow.children.length; + if (audioPlayersSpinBox.value < soundPlayersCount) { + // Remove extra sound effect players + soundEffectPlayersFlow.children.length = audioPlayersSpinBox.value; + return; + } + + if (audioPlayersSpinBox.value < soundPlayersCount) { + return; + } + // Create more sound effect players + for (var i = soundPlayersCount; i < audioPlayersSpinBox.value; i++) { + root.soundEffectPlayer.createObject(soundEffectPlayersFlow, { + index: i + }); + } + } + } + } + + ScrollView { + Layout.fillHeight: true + Layout.fillWidth: true + contentWidth: width + + background: Rectangle { + color: "#232323" + } + + CenteredFlow { + id: soundEffectPlayersFlow + + anchors.fill: parent + padding: 10 + spacing: 10 + } + } + } +} diff --git a/examples/tutorials/drumpad/initial_project/DrumpadContent/qmldir b/examples/tutorials/drumpad/initial_project/DrumpadContent/qmldir new file mode 100644 index 00000000..f1f34c52 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/DrumpadContent/qmldir @@ -0,0 +1,4 @@ +module DrumpadContent + +App 1.0 App.qml +MainScreen 1.0 MainScreen.qml diff --git a/examples/tutorials/drumpad/initial_project/Mocks/Audio/AudioEngine.qml b/examples/tutorials/drumpad/initial_project/Mocks/Audio/AudioEngine.qml new file mode 100644 index 00000000..4bfbc24f --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Mocks/Audio/AudioEngine.qml @@ -0,0 +1,27 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtMultimedia + +Item { + id: root + + property double volume + property url file + + MediaPlayer { + id: player + source: file + audioOutput: AudioOutput {} + } + + onVolumeChanged : { + console.log("Mock: VolumeChanaged ", volume ) + } + + function play() { + console.log("Mock: play()") + player.play() + } +} diff --git a/examples/tutorials/drumpad/initial_project/Mocks/Audio/WaveformItem.qml b/examples/tutorials/drumpad/initial_project/Mocks/Audio/WaveformItem.qml new file mode 100644 index 00000000..fcb6041b --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Mocks/Audio/WaveformItem.qml @@ -0,0 +1,13 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +Rectangle { + id: root + width: 1920 + height: 1080 + color: "blue" + property url file +} diff --git a/examples/tutorials/drumpad/initial_project/Mocks/Audio/qmldir b/examples/tutorials/drumpad/initial_project/Mocks/Audio/qmldir new file mode 100644 index 00000000..189d68e6 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Mocks/Audio/qmldir @@ -0,0 +1,3 @@ +module Audio +AudioEngine 1.0 AudioEngine.qml +WaveformItem 1.0 WaveformItem.qml diff --git a/examples/tutorials/drumpad/initial_project/Mocks/Components/AudioFilesModel.qml b/examples/tutorials/drumpad/initial_project/Mocks/Components/AudioFilesModel.qml new file mode 100644 index 00000000..b06a1e17 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Mocks/Components/AudioFilesModel.qml @@ -0,0 +1,8 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +Item { + getFiles: function() { + console.log("AudioFilesModel mock: getFiles()") + } +} diff --git a/examples/tutorials/drumpad/initial_project/Mocks/Components/qmldir b/examples/tutorials/drumpad/initial_project/Mocks/Components/qmldir new file mode 100644 index 00000000..2d8e9899 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Mocks/Components/qmldir @@ -0,0 +1,2 @@ +module Components +AudioFilesModel 1.0 AudioFilesModel.qml diff --git a/examples/tutorials/drumpad/initial_project/Sounds/Clap.wav b/examples/tutorials/drumpad/initial_project/Sounds/Clap.wav new file mode 100644 index 00000000..aceee331 Binary files /dev/null and b/examples/tutorials/drumpad/initial_project/Sounds/Clap.wav differ diff --git a/examples/tutorials/drumpad/initial_project/doc/drumpad_initial_project.pyproject b/examples/tutorials/drumpad/initial_project/doc/drumpad_initial_project.pyproject new file mode 100644 index 00000000..1841cd88 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/doc/drumpad_initial_project.pyproject @@ -0,0 +1,21 @@ +{ + "files": ["../Drumpad.qmlproject", + "../Drumpad.qrc", + "../qtquickcontrols2.conf", + "../Drumpad/AvailableSoundsComboBox.qml", + "../Drumpad/CenteredFlow.qml", + "../Drumpad/Constants.qml", + "../Drumpad/PadButton.qml", + "../Drumpad/qmldir", + "../Drumpad/SoundEffectPlayer.qml", + "../Drumpad/StyledSpinBox.qml", + "../Drumpad/VolumeSlider.qml", + "../DrumpadContent/App.qml", + "../DrumpadContent/MainScreen.qml", + "../DrumpadContent/qmldir", + "../Mocks/Audio/AudioEngine.qml", + "../Mocks/Audio/qmldir", + "../Mocks/Audio/WaveformItem.qml", + "../Mocks/Components/AudioFilesModel.qml", + "../Mocks/Components/qmldir"] +} diff --git a/examples/tutorials/drumpad/initial_project/doc/initial_project.md b/examples/tutorials/drumpad/initial_project/doc/initial_project.md new file mode 100644 index 00000000..b874d85e --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/doc/initial_project.md @@ -0,0 +1,12 @@ +# Drumpad example (Qt Design Studio) - Initial project + +This example contains the initial [Qt Design Studio] project to be used as a starting point for the +[Qt Design Studio integration tutorial]. **It is not an executable project as is**, since it does +**not** contain the required Python code developed along the tutorial. + +For more details, see the [Qt Design Studio integration tutorial]. + +To download the final project source code, visit {ref}`example_tutorials_drumpad_final_project`. + +[Qt Design Studio]: https://www.qt.io/product/ui-design-tools/ +[Qt Design Studio integration tutorial]: tutorial_qt_design_studio_integration diff --git a/examples/tutorials/drumpad/initial_project/qtquickcontrols2.conf b/examples/tutorials/drumpad/initial_project/qtquickcontrols2.conf new file mode 100644 index 00000000..87a95d01 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/qtquickcontrols2.conf @@ -0,0 +1,6 @@ +; This file can be edited to change the style of the application +; Read "Qt Quick Controls 2 Configuration File" for details: +; http://doc.qt.io/qt-5/qtquickcontrols2-configuration.html + +[Controls] +Style=Basic diff --git a/examples/utils/pyside_config.py b/examples/utils/pyside_config.py index c4bb873e..ea021030 100644 --- a/examples/utils/pyside_config.py +++ b/examples/utils/pyside_config.py @@ -49,10 +49,10 @@ 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), +options.append(("--shiboken-include-path", + lambda: get_package_include_path(Package.SHIBOKEN_MODULE), pyside_error, - "Print shiboken generator include paths")) + "Print shiboken module include paths")) options.append(("--pyside-include-path", lambda: get_package_include_path(Package.PYSIDE_MODULE), pyside_error, diff --git a/examples/webenginequick/nanobrowser/ApplicationRoot.qml b/examples/webenginequick/nanobrowser/ApplicationRoot.qml index f3624980..ec571620 100644 --- a/examples/webenginequick/nanobrowser/ApplicationRoot.qml +++ b/examples/webenginequick/nanobrowser/ApplicationRoot.qml @@ -1,22 +1,24 @@ // Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +pragma ComponentBehavior: Bound + import QtQuick import QtWebEngine QtObject { id: root - property QtObject defaultProfilePrototype : WebEngineProfilePrototype { + property WebEngineProfilePrototype defaultProfilePrototype : WebEngineProfilePrototype { storageName: "Profile" Component.onCompleted: { - let fullVersionList = defaultProfilePrototype.instance().clientHints.fullVersionList; + let fullVersionList = root.defaultProfilePrototype.instance().clientHints.fullVersionList; fullVersionList["QuickNanoBrowser"] = "1.0"; - defaultProfilePrototype.instance().clientHints.fullVersionList = fullVersionList; + root.defaultProfilePrototype.instance().clientHints.fullVersionList = fullVersionList; } } - property QtObject otrPrototype : WebEngineProfilePrototype { + property WebEngineProfilePrototype otrPrototype : WebEngineProfilePrototype { } property Component browserWindowComponent: BrowserWindow { @@ -26,18 +28,18 @@ QtObject { onClosing: destroy() } function createWindow(profile) { - var newWindow = browserWindowComponent.createObject(root); + var newWindow = browserWindowComponent.createObject(root) as BrowserWindow; newWindow.currentWebView.profile = profile; profile.downloadRequested.connect(newWindow.onDownloadRequested); return newWindow; } function createDialog(profile) { - var newDialog = browserDialogComponent.createObject(root); + var newDialog = browserDialogComponent.createObject(root) as BrowserDialog; newDialog.currentWebView.profile = profile; return newDialog; } function load(url) { - var browserWindow = createWindow(defaultProfilePrototype.instance()); + var browserWindow = createWindow(root.defaultProfilePrototype.instance()); browserWindow.currentWebView.url = url; } } diff --git a/examples/webenginequick/nanobrowser/BrowserWindow.qml b/examples/webenginequick/nanobrowser/BrowserWindow.qml index 365d77d2..474968b8 100644 --- a/examples/webenginequick/nanobrowser/BrowserWindow.qml +++ b/examples/webenginequick/nanobrowser/BrowserWindow.qml @@ -1,6 +1,8 @@ // Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +pragma ComponentBehavior: Bound + import QtCore import QtQml import QtQuick @@ -12,16 +14,17 @@ import QtWebEngine import BrowserUtils ApplicationWindow { - id: browserWindow - property QtObject applicationRoot - property Item currentWebView: tabBar.currentIndex < tabBar.count ? tabLayout.children[tabBar.currentIndex] : null + id: win + required property QtObject applicationRoot + property WebEngineView currentWebView: tabBar.currentIndex < tabBar.count ? tabLayout.children[tabBar.currentIndex] : null property int previousVisibility: Window.Windowed property int createdTabs: 0 + property bool lastTabClosing: false width: 1300 height: 900 visible: true - title: currentWebView && currentWebView.title + title: win.currentWebView?.title ?? "" // Make sure the Qt.WindowFullscreenButtonHint is set on OS X. Component.onCompleted: flags = flags | Qt.WindowFullscreenButtonHint @@ -31,7 +34,7 @@ ApplicationWindow { } // When using style "mac", ToolButtons are not supposed to accept focus. - property bool platformIsMac: Qt.platform.os == "osx" + property bool platformIsMac: Qt.platform.os === "osx" Settings { id : appSettings @@ -46,6 +49,8 @@ ApplicationWindow { property alias devToolsEnabled: devToolsEnabled.checked property alias pdfViewerEnabled: pdfViewerEnabled.checked property int imageAnimationPolicy: WebEngineSettings.ImageAnimationPolicy.Allow + property alias javascriptCanAccessClipboard: javascriptCanAccessClipboard.checked + property alias javascriptCanPaste: javascriptCanPaste.checked } Action { @@ -65,14 +70,16 @@ ApplicationWindow { Action { shortcut: StandardKey.Refresh onTriggered: { - if (currentWebView) - currentWebView.reload(); + if (win.currentWebView) + win.currentWebView.reload(); } } Action { shortcut: StandardKey.AddTab onTriggered: { - tabBar.createTab(tabBar.count != 0 ? currentWebView.profile : defaultProfilePrototype.instance()); + tabBar.createTab(tabBar.count !== 0 + ? win.currentWebView.profile + : (win.applicationRoot as ApplicationRoot).defaultProfilePrototype.instance()); addressBar.forceActiveFocus(); addressBar.selectAll(); } @@ -80,20 +87,20 @@ ApplicationWindow { Action { shortcut: StandardKey.Close onTriggered: { - currentWebView.triggerWebAction(WebEngineView.RequestClose); + win.currentWebView.triggerWebAction(WebEngineView.RequestClose); } } Action { shortcut: StandardKey.Quit - onTriggered: browserWindow.close() + onTriggered: win.close() } Action { shortcut: "Escape" onTriggered: { - if (currentWebView.state == "FullScreen") { - browserWindow.visibility = browserWindow.previousVisibility; + if (win.currentWebView.state === "FullScreen") { + win.visibility = win.previousVisibility; fullScreenNotification.hide(); - currentWebView.triggerWebAction(WebEngineView.ExitFullScreen); + win.currentWebView.triggerWebAction(WebEngineView.ExitFullScreen); } if (findBar.visible) @@ -102,52 +109,52 @@ ApplicationWindow { } Action { shortcut: "Ctrl+0" - onTriggered: currentWebView.zoomFactor = 1.0 + onTriggered: win.currentWebView.zoomFactor = 1.0 } Action { shortcut: StandardKey.ZoomOut - onTriggered: currentWebView.zoomFactor -= 0.1 + onTriggered: win.currentWebView.zoomFactor -= 0.1 } Action { shortcut: StandardKey.ZoomIn - onTriggered: currentWebView.zoomFactor += 0.1 + onTriggered: win.currentWebView.zoomFactor += 0.1 } Action { shortcut: StandardKey.Copy - onTriggered: currentWebView.triggerWebAction(WebEngineView.Copy) + onTriggered: win.currentWebView.triggerWebAction(WebEngineView.Copy) } Action { shortcut: StandardKey.Cut - onTriggered: currentWebView.triggerWebAction(WebEngineView.Cut) + onTriggered: win.currentWebView.triggerWebAction(WebEngineView.Cut) } Action { shortcut: StandardKey.Paste - onTriggered: currentWebView.triggerWebAction(WebEngineView.Paste) + onTriggered: win.currentWebView.triggerWebAction(WebEngineView.Paste) } Action { shortcut: "Shift+"+StandardKey.Paste - onTriggered: currentWebView.triggerWebAction(WebEngineView.PasteAndMatchStyle) + onTriggered: win.currentWebView.triggerWebAction(WebEngineView.PasteAndMatchStyle) } Action { shortcut: StandardKey.SelectAll - onTriggered: currentWebView.triggerWebAction(WebEngineView.SelectAll) + onTriggered: win.currentWebView.triggerWebAction(WebEngineView.SelectAll) } Action { shortcut: StandardKey.Undo - onTriggered: currentWebView.triggerWebAction(WebEngineView.Undo) + onTriggered: win.currentWebView.triggerWebAction(WebEngineView.Undo) } Action { shortcut: StandardKey.Redo - onTriggered: currentWebView.triggerWebAction(WebEngineView.Redo) + onTriggered: win.currentWebView.triggerWebAction(WebEngineView.Redo) } Action { shortcut: StandardKey.Back - onTriggered: currentWebView.triggerWebAction(WebEngineView.Back) + onTriggered: win.currentWebView.triggerWebAction(WebEngineView.Back) } Action { shortcut: StandardKey.Forward - onTriggered: currentWebView.triggerWebAction(WebEngineView.Forward) + onTriggered: win.currentWebView.triggerWebAction(WebEngineView.Forward) } Action { shortcut: StandardKey.Find @@ -170,16 +177,17 @@ ApplicationWindow { RowLayout { anchors.fill: parent ToolButton { - enabled: currentWebView && (currentWebView.canGoBack || currentWebView.canGoForward) + enabled: win.currentWebView?.canGoBack || win.currentWebView?.canGoForward onClicked: historyMenu.open() text: qsTr("▼") Menu { id: historyMenu Instantiator { - model: currentWebView && currentWebView.history.items + model: win.currentWebView?.history?.items MenuItem { + required property var model text: model.title - onTriggered: currentWebView.goBackOrForward(model.offset) + onTriggered: win.currentWebView.goBackOrForward(model.offset) checkable: !enabled checked: !enabled enabled: model.offset @@ -197,23 +205,25 @@ ApplicationWindow { ToolButton { id: backButton - icon.source: "qrc:/icons/go-previous.png" - onClicked: currentWebView.goBack() - enabled: currentWebView && currentWebView.canGoBack - activeFocusOnTab: !browserWindow.platformIsMac + icon.source: "icons/3rdparty/go-previous.png" + onClicked: win.currentWebView.goBack() + enabled: win.currentWebView?.canGoBack ?? false + activeFocusOnTab: !win.platformIsMac } ToolButton { id: forwardButton - icon.source: "qrc:/icons/go-next.png" - onClicked: currentWebView.goForward() - enabled: currentWebView && currentWebView.canGoForward - activeFocusOnTab: !browserWindow.platformIsMac + icon.source: "icons/3rdparty/go-next.png" + onClicked: win.currentWebView.goForward() + enabled: win.currentWebView?.canGoForward ?? false + activeFocusOnTab: !win.platformIsMac } ToolButton { id: reloadButton - icon.source: currentWebView && currentWebView.loading ? "qrc:/icons/process-stop.png" : "qrc:/icons/view-refresh.png" - onClicked: currentWebView && currentWebView.loading ? currentWebView.stop() : currentWebView.reload() - activeFocusOnTab: !browserWindow.platformIsMac + icon.source: win.currentWebView?.loading + ? "icons/3rdparty/process-stop.png" + : "icons/3rdparty/view-refresh.png" + onClicked: win.currentWebView?.loading ? win.currentWebView.stop() : win.currentWebView.reload() + activeFocusOnTab: !win.platformIsMac } TextField { id: addressBar @@ -224,7 +234,7 @@ ApplicationWindow { id: faviconImage width: 16; height: 16 sourceSize: Qt.size(width, height) - source: currentWebView && currentWebView.icon ? currentWebView.icon : '' + source: win.currentWebView?.icon ? win.currentWebView.icon : '' } MouseArea { id: textFieldMouseArea @@ -272,10 +282,10 @@ ApplicationWindow { focus: true Layout.fillWidth: true Binding on text { - when: currentWebView - value: currentWebView.url + when: win.currentWebView + value: win.currentWebView.url } - onAccepted: currentWebView.url = Utils.fromUserInput(text) + onAccepted: win.currentWebView.url = Utils.fromUserInput(text) selectByMouse: true } ToolButton { @@ -319,21 +329,25 @@ ApplicationWindow { id: offTheRecordEnabled text: "Off The Record" checkable: true - checked: currentWebView && currentWebView.profile === otrPrototype.instance() - onToggled: function(checked) { - if (currentWebView) { - currentWebView.profile = checked ? otrPrototype.instance() : defaultProfilePrototype.instance(); + checked: win.currentWebView?.profile === (win.applicationRoot as ApplicationRoot).otrPrototype.instance() + onToggled: function() { + if (win.currentWebView) { + win.currentWebView.profile = offTheRecordEnabled.checked + ? (win.applicationRoot as ApplicationRoot).otrPrototype.instance() + : (win.applicationRoot as ApplicationRoot).defaultProfilePrototype.instance(); } } } MenuItem { id: httpDiskCacheEnabled text: "HTTP Disk Cache" - checkable: currentWebView && !currentWebView.profile.offTheRecord - checked: currentWebView && (currentWebView.profile.httpCacheType === WebEngineProfile.DiskHttpCache) - onToggled: function(checked) { - if (currentWebView) { - currentWebView.profile.httpCacheType = checked ? WebEngineProfile.DiskHttpCache : WebEngineProfile.MemoryHttpCache; + checkable: !win.currentWebView?.profile?.offTheRecord ?? false + checked: win.currentWebView?.profile.httpCacheType === WebEngineProfile.DiskHttpCache + onToggled: function() { + if (win.currentWebView) { + win.currentWebView.profile.httpCacheType = httpDiskCacheEnabled.checked + ? WebEngineProfile.DiskHttpCache + : WebEngineProfile.MemoryHttpCache; } } } @@ -368,7 +382,6 @@ ApplicationWindow { checkable: true checked: WebEngine.settings.pdfViewerEnabled } - Menu { id: imageAnimationPolicy title: "Image Animation Policy" @@ -407,6 +420,18 @@ ApplicationWindow { } } + MenuItem { + id: javascriptCanAccessClipboard + text: "JavaScript can access clipboard" + checkable: true + checked: WebEngine.settings.javascriptCanAccessClipboard + } + MenuItem { + id: javascriptCanPaste + text: "JavaScript can paste" + checkable: true + checked: WebEngine.settings.javascriptCanPaste + } } } } @@ -417,14 +442,14 @@ ApplicationWindow { left: parent.left top: parent.bottom right: parent.right - leftMargin: parent.leftMargin - rightMargin: parent.rightMargin + leftMargin: parent.anchors.leftMargin + rightMargin: parent.anchors.rightMargin } background: Item {} z: -2 from: 0 to: 100 - value: (currentWebView && currentWebView.loadProgress < 100) ? currentWebView.loadProgress : 0 + value: (win.currentWebView?.loadProgress < 100) ? win.currentWebView.loadProgress : 0 } } @@ -442,22 +467,22 @@ ApplicationWindow { id: tabButtonComponent TabButton { - property color frameColor: "#999" - property color fillColor: "#eee" - property color nonSelectedColor: "#ddd" + id: tabButton + property color frameColor: "#999999" + property color fillColor: "#eeeeee" + property color nonSelectedColor: "#dddddd" property string tabTitle: "New Tab" - id: tabButton contentItem: Rectangle { id: tabRectangle - color: tabButton.down ? fillColor : nonSelectedColor + color: tabButton.down ? tabButton.fillColor : tabButton.nonSelectedColor border.width: 1 - border.color: frameColor + border.color: tabButton.frameColor implicitWidth: Math.max(text.width + 30, 80) implicitHeight: Math.max(text.height + 10, 20) - Rectangle { height: 1 ; width: parent.width ; color: frameColor} - Rectangle { height: parent.height ; width: 1; color: frameColor} - Rectangle { x: parent.width - 2; height: parent.height ; width: 1; color: frameColor} + Rectangle { height: 1 ; width: parent.width ; color: tabButton.frameColor} + Rectangle { height: parent.height ; width: 1; color: tabButton.frameColor} + Rectangle { x: parent.width - 2; height: parent.height ; width: 1; color: tabButton.frameColor} Text { id: text anchors.left: parent.left @@ -465,7 +490,7 @@ ApplicationWindow { anchors.leftMargin: 6 text: tabButton.tabTitle elide: Text.ElideRight - color: tabButton.down ? "black" : frameColor + color: tabButton.down ? "black" : tabButton.frameColor width: parent.width - button.background.width } Button { @@ -477,16 +502,16 @@ ApplicationWindow { background: Rectangle { implicitWidth: 12 implicitHeight: 12 - color: button.hovered ? "#ccc" : tabRectangle.color + color: button.hovered ? "#cccccc" : tabRectangle.color Text {text: "x"; anchors.centerIn: parent; color: "gray"} } onClicked: tabButton.closeTab() } } - onClicked: addressBar.text = tabLayout.itemAt(TabBar.index).url; + onClicked: addressBar.text = (tabLayout.itemAt(TabBar.index) as WebEngineView).url; function closeTab() { - tabBar.removeView(TabBar.index); + tabBar.tryCloseView(TabBar.index); } } } @@ -496,10 +521,10 @@ ApplicationWindow { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - Component.onCompleted: createTab(defaultProfilePrototype.instance()) + Component.onCompleted: createTab((win.applicationRoot as ApplicationRoot).defaultProfilePrototype.instance()) function createTab(profile, focusOnNewTab = true, url = undefined) { - var webview = tabComponent.createObject(tabLayout, {profile: profile}); + var webview = tabComponent.createObject(tabLayout, {index: tabBar.count , profile: profile}); var newTabButton = tabButtonComponent.createObject(tabBar, {tabTitle: Qt.binding(function () { return webview.title; })}); tabBar.addItem(newTabButton); if (focusOnNewTab) { @@ -511,12 +536,17 @@ ApplicationWindow { return webview; } + function tryCloseView(index) { + tabLayout.children[index].triggerWebAction(WebEngineView.RequestClose); + } + function removeView(index) { if (tabBar.count > 1) { tabBar.removeItem(tabBar.itemAt(index)); tabLayout.children[index].destroy(); } else { - browserWindow.close(); + win.lastTabClosing = true; + win.close(); } } @@ -524,10 +554,11 @@ ApplicationWindow { id: tabComponent WebEngineView { id: webEngineView + property int index; focus: true onLinkHovered: function(hoveredUrl) { - if (hoveredUrl == "") + if (hoveredUrl === "") hideStatusText.start(); else { statusText.text = hoveredUrl; @@ -563,6 +594,12 @@ ApplicationWindow { settings.pdfViewerEnabled: appSettings.pdfViewerEnabled settings.imageAnimationPolicy: appSettings.imageAnimationPolicy settings.screenCaptureEnabled: true + settings.javascriptCanAccessClipboard: appSettings.javascriptCanAccessClipboard + settings.javascriptCanPaste: appSettings.javascriptCanPaste + + onWindowCloseRequested: function() { + tabBar.removeView(webEngineView.index); + } onCertificateError: function(error) { if (!error.isMainFrame) { @@ -578,29 +615,29 @@ ApplicationWindow { if (!request.userInitiated) console.warn("Blocked a popup window."); else if (request.destination === WebEngineNewWindowRequest.InNewTab) { - var tab = tabBar.createTab(currentWebView.profile, true, request.requestedUrl); + var tab = tabBar.createTab(win.currentWebView.profile, true, request.requestedUrl); tab.acceptAsNewWindow(request); } else if (request.destination === WebEngineNewWindowRequest.InNewBackgroundTab) { - var backgroundTab = tabBar.createTab(currentWebView.profile, false); + var backgroundTab = tabBar.createTab(win.currentWebView.profile, false); backgroundTab.acceptAsNewWindow(request); } else if (request.destination === WebEngineNewWindowRequest.InNewDialog) { - var dialog = applicationRoot.createDialog(currentWebView.profile); - dialog.currentWebView.acceptAsNewWindow(request); + var dialog = (win.applicationRoot as ApplicationRoot).createDialog(win.currentWebView.profile); + dialog.win.currentWebView.acceptAsNewWindow(request); } else { - var window = applicationRoot.createWindow(currentWebView.profile); - window.currentWebView.acceptAsNewWindow(request); + var window = (win.applicationRoot as ApplicationRoot).createWindow(win.currentWebView.profile); + window.win.currentWebView.acceptAsNewWindow(request); } } onFullScreenRequested: function(request) { if (request.toggleOn) { webEngineView.state = "FullScreen"; - browserWindow.previousVisibility = browserWindow.visibility; - browserWindow.showFullScreen(); + win.previousVisibility = win.visibility; + win.showFullScreen(); fullScreenNotification.show(); } else { webEngineView.state = ""; - browserWindow.visibility = browserWindow.previousVisibility; + win.visibility = win.previousVisibility; fullScreenNotification.hide(); } request.accept(); @@ -651,7 +688,7 @@ ApplicationWindow { } onLoadingChanged: function(loadRequest) { - if (loadRequest.status == WebEngineView.LoadStartedStatus) + if (loadRequest.status === WebEngineView.LoadStartedStatus) findBar.reset(); } @@ -668,7 +705,7 @@ ApplicationWindow { interval: 0 running: false repeat: false - onTriggered: currentWebView.reload() + onTriggered: win.currentWebView.reload() } } } @@ -682,7 +719,7 @@ ApplicationWindow { anchors.right: parent.right anchors.bottom: parent.bottom onNewWindowRequested: function(request) { - var tab = tabBar.createTab(currentWebView.profile); + var tab = tabBar.createTab(win.currentWebView.profile); request.openIn(tab); } @@ -693,7 +730,7 @@ ApplicationWindow { repeat: false onTriggered: devToolsEnabled.checked = false } - onWindowCloseRequested: function(request) { + onWindowCloseRequested: function() { // Delay hiding for keep the inspectedView set to receive the ACK message of close. hideTimer.running = true; } @@ -744,7 +781,7 @@ ApplicationWindow { Dialog { id: permissionDialog anchors.centerIn: parent - width: Math.min(browserWindow.width, browserWindow.height) / 3 * 2 + width: Math.min(win.width, win.height) / 3 * 2 contentWidth: mainTextForPermissionDialog.width contentHeight: mainTextForPermissionDialog.height standardButtons: Dialog.No | Dialog.Yes @@ -859,13 +896,13 @@ ApplicationWindow { onFindNext: { if (text) - currentWebView && currentWebView.findText(text); + win.currentWebView?.findText(text); else if (!visible) visible = true; } onFindPrevious: { if (text) - currentWebView && currentWebView.findText(text, WebEngineView.FindBackward); + win.currentWebView?.findText(text, WebEngineView.FindBackward); else if (!visible) visible = true; } @@ -898,4 +935,14 @@ ApplicationWindow { } } } + + onClosing: function(closeEvent) { + if (lastTabClosing) { + return; + } + closeEvent.accepted = false + for (var i = 0; i < tabBar.count; i++) { + tabBar.tryCloseView(i); + } + } } diff --git a/examples/webenginequick/nanobrowser/DownloadView.qml b/examples/webenginequick/nanobrowser/DownloadView.qml index b116ab86..ef0c7f5a 100644 --- a/examples/webenginequick/nanobrowser/DownloadView.qml +++ b/examples/webenginequick/nanobrowser/DownloadView.qml @@ -1,10 +1,10 @@ // Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Controls.Fusion -import QtWebEngine -import QtQuick.Layouts Rectangle { id: downloadView @@ -25,20 +25,26 @@ Rectangle { id: downloadItemDelegate Rectangle { + id: downloadItem width: listView.width height: childrenRect.height anchors.margins: 10 radius: 3 color: "transparent" border.color: "black" + + required property int index + Rectangle { id: progressBar - property real progress: downloadModel.downloads[index] - ? downloadModel.downloads[index].receivedBytes / downloadModel.downloads[index].totalBytes : 0 + property real progress: { + let d = downloadModel.downloads[downloadItem.index] + return d ? d.receivedBytes / d.totalBytes : 0 + } radius: 3 - color: width == listView.width ? "green" : "#2b74c7" + color: width === listView.width ? "green" : "#2b74c7" width: listView.width * progress height: cancelButton.height @@ -54,7 +60,10 @@ Rectangle { } Label { id: label - text: downloadModel.downloads[index] ? downloadModel.downloads[index].downloadDirectory + "/" + downloadModel.downloads[index].downloadFileName : qsTr("") + text: { + let d = downloadModel.downloads[downloadItem.index] + return d ? d.downloadDirectory + "/" + d.downloadFileName : qsTr("") + } anchors { verticalCenter: cancelButton.verticalCenter left: parent.left @@ -64,16 +73,16 @@ Rectangle { Button { id: cancelButton anchors.right: parent.right - icon.source: "qrc:/icons/process-stop.png" + icon.source: "icons/3rdparty/process-stop.png" onClicked: { - var download = downloadModel.downloads[index]; + var download = downloadModel.downloads[downloadItem.index]; download.cancel(); downloadModel.downloads = downloadModel.downloads.filter(function (el) { return el.id !== download.id; }); - downloadModel.remove(index); + downloadModel.remove(downloadItem.index); } } } diff --git a/examples/webenginequick/nanobrowser/FindBar.qml b/examples/webenginequick/nanobrowser/FindBar.qml index 409d8dcf..013f28e8 100644 --- a/examples/webenginequick/nanobrowser/FindBar.qml +++ b/examples/webenginequick/nanobrowser/FindBar.qml @@ -63,46 +63,47 @@ Rectangle { } Label { - text: activeMatch + "/" + numberOfMatches - visible: findTextField.text != "" + text: root.activeMatch + "/" + root.numberOfMatches + visible: findTextField.text !== "" color: "black" } Rectangle { border.width: 1 - border.color: "#ddd" - width: 2 - height: parent.height - anchors.topMargin: 5 - anchors.bottomMargin: 5 + border.color: "#dddddd" + Layout.preferredWidth: 2 + Layout.preferredHeight: parent.height } ToolButton { + id: findBtnLeft text: "<" - enabled: numberOfMatches > 0 + enabled: root.numberOfMatches > 0 onClicked: root.findPrevious() contentItem: Text { color: "black" - text: parent.text + text: findBtnLeft.text } } ToolButton { + id: findBtnRight text: ">" - enabled: numberOfMatches > 0 + enabled: root.numberOfMatches > 0 onClicked: root.findNext() contentItem: Text { color: "black" - text: parent.text + text: findBtnRight.text } } ToolButton { + id: findBtnClose text: "x" onClicked: root.visible = false contentItem: Text { color: "black" - text: parent.text + text: findBtnClose.text } } } diff --git a/examples/webenginequick/nanobrowser/FullScreenNotification.qml b/examples/webenginequick/nanobrowser/FullScreenNotification.qml index 77940643..cdf154c7 100644 --- a/examples/webenginequick/nanobrowser/FullScreenNotification.qml +++ b/examples/webenginequick/nanobrowser/FullScreenNotification.qml @@ -28,8 +28,8 @@ Rectangle { NumberAnimation { duration: 750 onStopped: { - if (opacity == 0) - visible = false; + if (fullScreenNotification.opacity === 0) + fullScreenNotification.visible = false; } } } @@ -37,7 +37,7 @@ Rectangle { Timer { id: reset interval: 5000 - onTriggered: hide() + onTriggered: fullScreenNotification.hide() } anchors.horizontalCenter: parent.horizontalCenter diff --git a/examples/webenginequick/nanobrowser/WebAuthDialog.qml b/examples/webenginequick/nanobrowser/WebAuthDialog.qml new file mode 100644 index 00000000..8d5cf598 --- /dev/null +++ b/examples/webenginequick/nanobrowser/WebAuthDialog.qml @@ -0,0 +1,285 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtWebEngine + +Dialog { + id: webAuthDialog + anchors.centerIn: parent + width: Math.min(parent.parent.width, parent.parent.height) / 3 * 2 + contentWidth: verticalLayout.width +10; + contentHeight: verticalLayout.height +10; + standardButtons: Dialog.Cancel | Dialog.Apply + title: "WebAuth Request" + + property var selectAccount; + property var authrequest: null; + + Connections { + id: webauthConnection + ignoreUnknownSignals: true + function onStateChanged(state) { + webAuthDialog.setupUI(state); + } + } + + onApplied: { + switch (webAuthDialog.authrequest.state) { + case WebEngineWebAuthUxRequest.WebAuthUxState.CollectPin: + webAuthDialog.authrequest.setPin(pinEdit.text); + break; + case WebEngineWebAuthUxRequest.WebAuthUxState.SelectAccount: + webAuthDialog.authrequest.setSelectedAccount(webAuthDialog.selectAccount); + break; + default: + break; + } + } + + onRejected: { + webAuthDialog.authrequest.cancel(); + } + + function init(request) { + pinLabel.visible = false; + pinEdit.visible = false; + confirmPinLabel.visible = false; + confirmPinEdit.visible = false; + selectAccountModel.clear(); + webAuthDialog.authrequest = request; + webauthConnection.target = request; + setupUI(webAuthDialog.authrequest.state) + webAuthDialog.visible = true; + pinEntryError.visible = false; + } + + function setupUI(state) { + switch (state) { + case WebEngineWebAuthUxRequest.WebAuthUxState.SelectAccount: + setupSelectAccountUI(); + break; + case WebEngineWebAuthUxRequest.WebAuthUxState.CollectPin: + setupCollectPin(); + break; + case WebEngineWebAuthUxRequest.WebAuthUxState.FinishTokenCollection: + setupFinishCollectToken(); + break; + case WebEngineWebAuthUxRequest.WebAuthUxState.RequestFailed: + setupErrorUI(); + break; + case WebEngineWebAuthUxRequest.WebAuthUxState.Completed: + webAuthDialog.close(); + break; + } + } + + ButtonGroup { + id : selectAccount; + exclusive: true; + } + + ListModel { + id: selectAccountModel + + } + contentItem: Item { + ColumnLayout { + id : verticalLayout + spacing : 10 + + Label { + id: heading + text: ""; + } + + Label { + id: description + text: ""; + } + + Row { + spacing : 10 + Label { + id: pinLabel + text: "PIN"; + } + TextInput { + id: pinEdit + text: "EnterPin" + enabled: true + focus: true + color: "white" + layer.sourceRect: Qt.rect(0, 0, 20, 20) + } + } + + Row { + spacing : 10 + Label { + id: confirmPinLabel + text: "Confirm PIN"; + } + TextEdit { + id: confirmPinEdit + text: "" + } + } + + Label { + id: pinEntryError + text: ""; + } + + Repeater { + id : selectAccountRepeater + model: selectAccountModel + Column { + id: selectAccountRepeaterColumn + required property string modelData + spacing : 5 + RadioButton { + text: selectAccountRepeaterColumn.modelData + ButtonGroup.group : webAuthDialog.selectAccount; + onClicked: function(){ + webAuthDialog.selectAccount = text; + } + } + } + } + } + } + + function setupSelectAccountUI() { + webAuthDialog.selectAccount = ""; + heading.text = "Choose a passkey"; + description.text = "Which passkey do you want to use for " + webAuthDialog.authrequest.relyingPartyId; + + selectAccountModel.clear(); + var userNames = webAuthDialog.authrequest.userNames; + for (var i = 0; i < userNames.length; i++) { + selectAccountModel.append( {"name" : userNames[i]}); + } + pinLabel.visible = false; + pinEdit.visible = false; + confirmPinLabel.visible = false; + confirmPinEdit.visible = false; + pinEntryError.visible = false; + standardButton(Dialog.Apply).visible = true; + standardButton(Dialog.Cancel).visible = true; + standardButton(Dialog.Cancel).text ="Cancel" + } + + function setupCollectPin() { + var requestInfo = webAuthDialog.authrequest.pinRequest; + + pinEdit.clear(); + + if (requestInfo.reason === WebEngineWebAuthUxRequest.PinEntryReason.Challenge) { + heading.text = "PIN required"; + description.text = "Enter the PIN for your security key"; + pinLabel.visible = true; + pinEdit.visible = true; + confirmPinLabel.visible = false; + confirmPinEdit.visible = false; + } else if (requestInfo.reason === WebEngineWebAuthUxRequest.PinEntryReason.Set) { + heading.text = "Set PIN "; + description.text = "Set new PIN for your security key"; + pinLabel.visible = true; + pinEdit.visible = true; + confirmPinLabel.visible = true; + confirmPinEdit.visible = true; + } + pinEntryError.text = getPINErrorDetails() + " " + requestInfo.remainingAttempts + " attempts reamining"; + pinEntryError.visible = true; + selectAccountModel.clear(); + standardButton(Dialog.Cancel).visible = true; + standardButton(Dialog.Cancel).text ="Cancel" + standardButton(Dialog.Apply).visible = true; + } + + function getPINErrorDetails() { + var requestInfo = webAuthDialog.authrequest.pinRequest; + switch (requestInfo.error) { + case WebEngineWebAuthUxRequest.PinEntryError.NoError: + return ""; + case WebEngineWebAuthUxRequest.PinEntryError.TooShort: + return "Too short"; + case WebEngineWebAuthUxRequest.PinEntryError.InternalUvLocked: + return "Internal Uv locked"; + case WebEngineWebAuthUxRequest.PinEntryError.WrongPin: + return "Wrong PIN"; + case WebEngineWebAuthUxRequest.PinEntryError.InvalidCharacters: + return "Invalid characters"; + case WebEngineWebAuthUxRequest.PinEntryError.SameAsCurrentPin: + return "Same as current PIN"; + } + } + + function getRequestFailureResaon() { + var requestFailureReason = webAuthDialog.authrequest.requestFailureReason; + switch (requestFailureReason) { + case WebEngineWebAuthUxRequest.RequestFailureReason.Timeout: + return " Request Timeout"; + case WebEngineWebAuthUxRequest.RequestFailureReason.KeyNotRegistered: + return "Key not registered"; + case WebEngineWebAuthUxRequest.RequestFailureReason.KeyAlreadyRegistered: + return "You already registered this device. You don't have to register it again\n" + + "Try again with different key or device."; + case WebEngineWebAuthUxRequest.RequestFailureReason.SoftPinBlock: + return "The security key is locked because the wrong PIN was entered too many times.\n" + + "To unlock it, remove and reinsert it."; + case WebEngineWebAuthUxRequest.RequestFailureReason.HardPinBlock: + return "The security key is locked because the wrong PIN was entered too many times.\n" + + "You'll need to reset the security key."; + case WebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorRemovedDuringPinEntry: + return "Authenticator removed during verification. Please reinsert and try again"; + case WebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorMissingResidentKeys: + return "Authenticator doesn't have resident key support"; + case WebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorMissingUserVerification: + return "Authenticator missing user verification"; + case WebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorMissingLargeBlob: + return "Authenticator missing Large Blob support"; + case WebEngineWebAuthUxRequest.RequestFailureReason.NoCommonAlgorithms: + return "No common Algorithms"; + case WebEngineWebAuthUxRequest.RequestFailureReason.StorageFull: + return "Storage full"; + case WebEngineWebAuthUxRequest.RequestFailureReason.UserConsentDenied: + return "User consent denied"; + case WebEngineWebAuthUxRequest.RequestFailureReason.WinUserCancelled: + return "User cancelled request"; + } + } + + function setupFinishCollectToken() { + heading.text = "Use your security key with " + webAuthDialog.authrequest.relyingPartyId; + description.text = "Touch your security key again to complete the request."; + pinLabel.visible = false; + pinEdit.visible = false; + confirmPinLabel.visible = false; + confirmPinEdit.visible = false; + selectAccountModel.clear(); + pinEntryError.visible = false; + standardButton(Dialog.Apply).visible = false; + standardButton(Dialog.Cancel).visible = true; + standardButton(Dialog.Cancel).text ="Cancel" + } + + function setupErrorUI() { + heading.text = "Something went wrong"; + description.text = getRequestFailureResaon(); + pinLabel.visible = false; + pinEdit.visible = false; + confirmPinLabel.visible = false; + confirmPinEdit.visible = false; + selectAccountModel.clear(); + pinEntryError.visible = false; + standardButton(Dialog.Apply).visible = false; + standardButton(Dialog.Cancel).visible = true; + standardButton(Dialog.Cancel).text ="Close" + } +} diff --git a/examples/webenginequick/nanobrowser/nanobrowser.pyproject b/examples/webenginequick/nanobrowser/nanobrowser.pyproject index c86c57f6..392748d6 100644 --- a/examples/webenginequick/nanobrowser/nanobrowser.pyproject +++ b/examples/webenginequick/nanobrowser/nanobrowser.pyproject @@ -1,6 +1,6 @@ { "files": ["quicknanobrowser.py", "ApplicationRoot.qml", "BrowserDialog.qml", "BrowserWindow.qml", "DownloadView.qml", - "FindBar.qml", "FullScreenNotification.qml", + "FindBar.qml", "FullScreenNotification.qml", "WebAuthDialog.qml", "resources.qrc"] } diff --git a/examples/webenginequick/nanobrowser/qmldir b/examples/webenginequick/nanobrowser/qmldir new file mode 100644 index 00000000..d4d0abdc --- /dev/null +++ b/examples/webenginequick/nanobrowser/qmldir @@ -0,0 +1,9 @@ +module BrowserUtils +ApplicationRoot 254.0 ApplicationRoot.qml +BrowserDialog 254.0 BrowserDialog.qml +BrowserWindow 254.0 BrowserWindow.qml +DownloadView 254.0 DownloadView.qml +FindBar 254.0 FindBar.qml +FullScreenNotification 254.0 FullScreenNotification.qml +WebAuthDialog 254.0 WebAuthDialog.qml +depends QtQuick diff --git a/examples/webenginequick/nanobrowser/rc_resources.py b/examples/webenginequick/nanobrowser/rc_resources.py index 990f1027..b517c39a 100644 --- a/examples/webenginequick/nanobrowser/rc_resources.py +++ b/examples/webenginequick/nanobrowser/rc_resources.py @@ -1,6 +1,6 @@ # Resource object code (Python 3) # Created by: object code -# Created by: The Resource Compiler for Qt version 6.4.0 +# Created by: The Resource Compiler for Qt version 6.10.0 # WARNING! All changes made in this file will be lost! from PySide6 import QtCore @@ -330,13 +330,13 @@ qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00,\x00\x00\x00\x00\x00\x01\x00\x00\x03\xa6\ -\x00\x00\x01{\xe0\xa8\xe4\xe2\ +\x00\x00\x01\x975l\xc7\xfb\ \x00\x00\x00R\x00\x00\x00\x00\x00\x01\x00\x00\x08\xfe\ -\x00\x00\x01{\xe0\xa8\xe4\xe2\ +\x00\x00\x01\x975l\xc7\xfb\ \x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01{\xe0\xa8\xe4\xe2\ +\x00\x00\x01\x975l\xc7\xfb\ \x00\x00\x00x\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xfa\ -\x00\x00\x01{\xe0\xa8\xe4\xe2\ +\x00\x00\x01\x975l\xc7\xfb\ " def qInitResources(): diff --git a/examples/widgetbinding/CMakeLists.txt b/examples/widgetbinding/CMakeLists.txt index 1c5eefa5..acaa897d 100644 --- a/examples/widgetbinding/CMakeLists.txt +++ b/examples/widgetbinding/CMakeLists.txt @@ -50,66 +50,24 @@ set(generated_sources ${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() - +# ================================== Dependency detection ====================================== + +# Find required packages +find_package(Python COMPONENTS Interpreter Development REQUIRED) +# On RHEL and some other distros, Python wheels and site-packages may be installed under 'lib64' +# instead of 'lib'. The FindPython CMake module may set Python_SITELIB to 'lib', which is incorrect +# for these cases. To ensure compatibility, we override Python_SITELIB by querying Python directly. +# This guarantees the correct site-packages path is used regardless of platform or Python build. +execute_process( + COMMAND ${Python_EXECUTABLE} -c + "import site; print(next(p for p in site.getsitepackages() if 'site-packages' in p))" + OUTPUT_VARIABLE Python_SITELIB + OUTPUT_STRIP_TRAILING_WHITESPACE +) +list(APPEND CMAKE_PREFIX_PATH + "${Python_SITELIB}/shiboken6_generator/lib/cmake" +) +find_package(Shiboken6Tools REQUIRED) # ==================================== RPATH configuration ==================================== @@ -121,7 +79,7 @@ endif() # 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 ${CMAKE_CURRENT_SOURCE_DIR}) set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) # ============================================================================================= # !!! End of dubious section. @@ -165,81 +123,23 @@ set_property(TARGET ${wiggly_library} PROPERTY PREFIX "") # library can't link to the wiggly library. target_compile_definitions(${wiggly_library} PRIVATE BINDINGS_BUILD) +target_link_libraries(${wiggly_library} PRIVATE Qt6::Widgets) -# ====================== 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}) +# ====================== Shiboken target for generating binding C++ files ==================== +# Define Qt modules needed +set(qt_modules Core Gui Widgets) -# 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() +# Create Python bindings using Shiboken6Tools function +shiboken_generator_create_binding( + EXTENSION_TARGET ${bindings_library} + GENERATED_SOURCES ${generated_sources} + HEADERS ${wrapped_header} + TYPESYSTEM_FILE ${typesystem_file} + LIBRARY_TARGET ${wiggly_library} + QT_MODULES Core Gui Widgets +) # ================================= Dubious deployment section ================================ @@ -266,15 +166,14 @@ if(WIN32) 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. + # Get the correct DLL path for the current build type + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + get_target_property(dll_path Shiboken6::libshiboken IMPORTED_LOCATION_DEBUG) + else() + get_target_property(dll_path Shiboken6::libshiboken IMPORTED_LOCATION_RELEASE) + endif() + file(TO_CMAKE_PATH "${dll_path}" dll_path) + set(windows_shiboken_shared_libraries "${dll_path}") # ========================================================================================= endif() diff --git a/examples/widgetbinding/doc/widgetbinding.md b/examples/widgetbinding/doc/widgetbinding.md index 910961b1..5f8232fe 100644 --- a/examples/widgetbinding/doc/widgetbinding.md +++ b/examples/widgetbinding/doc/widgetbinding.md @@ -34,8 +34,6 @@ The most important files are: * `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`: diff --git a/examples/widgetbinding/doc/widgetbinding.pyproject b/examples/widgetbinding/doc/widgetbinding.pyproject index da4219d8..ce3f1fae 100644 --- a/examples/widgetbinding/doc/widgetbinding.pyproject +++ b/examples/widgetbinding/doc/widgetbinding.pyproject @@ -8,6 +8,5 @@ "../wigglywidget.cpp", "../wigglywidget.h", "../wigglywidget.py", - "../CMakeLists.txt", - "../../utils/pyside_config.py"] + "../CMakeLists.txt"] } diff --git a/examples/widgets/animation/easing/easing.py b/examples/widgets/animation/easing/easing.py index 5ae9c0be..ccb29f38 100644 --- a/examples/widgets/animation/easing/easing.py +++ b/examples/widgets/animation/easing/easing.py @@ -22,9 +22,21 @@ class PathType(IntEnum): CIRCLE_PATH = 1 +def createEasingCurve(curveType): + curve = QEasingCurve(curveType) + if curveType == QEasingCurve.Type.BezierSpline: + curve.addCubicBezierSegment(QPointF(0.4, 0.1), QPointF(0.6, 0.9), QPointF(1.0, 1.0)) + elif curveType == QEasingCurve.Type.TCBSpline: + curve.addTCBSegment(QPointF(0.0, 0.0), 0, 0, 0) + curve.addTCBSegment(QPointF(0.3, 0.4), 0.2, 1, -0.2) + curve.addTCBSegment(QPointF(0.7, 0.6), -0.2, 1, 0.2) + curve.addTCBSegment(QPointF(1.0, 1.0), 0, 0, 0) + return curve + + class Animation(QPropertyAnimation): - def __init__(self, target, prop): - super().__init__(target, prop) + def __init__(self, target, prop, parent=None): + super().__init__(target, prop, parent) self.set_path_type(PathType.LINEAR_PATH) def set_path_type(self, pathType): @@ -109,7 +121,7 @@ class Window(QWidget): self._scene.addItem(self._item.pixmap_item) self._ui.graphicsView.setScene(self._scene) - self._anim = Animation(self._item, b'pos') + self._anim = Animation(self._item, b'pos', self) self._anim.setEasingCurve(QEasingCurve.Type.OutBounce) self._ui.easingCurvePicker.setCurrentRow(0) @@ -124,13 +136,15 @@ class Window(QWidget): brush = QBrush(gradient) - curve_types = [(f"QEasingCurve.{e.name}", e) for e in QEasingCurve.Type if e.value <= 40] + curve_count = QEasingCurve.Type.Custom.value + curve_types = [(f"QEasingCurve.{e.name}", e) + for e in QEasingCurve.Type if e.value < curve_count] with QPainter(pix) as painter: for curve_name, curve_type in curve_types: painter.fillRect(QRect(QPoint(0, 0), self._iconSize), brush) - curve = QEasingCurve(curve_type) + curve = createEasingCurve(curve_type) painter.setPen(QColor(0, 0, 255, 64)) x_axis = self._iconSize.height() / 1.5 @@ -180,7 +194,7 @@ class Window(QWidget): def curve_changed(self, row): curve_type = QEasingCurve.Type(row) - self._anim.setEasingCurve(curve_type) + self._anim.setEasingCurve(createEasingCurve(curve_type)) self._anim.setCurrentTime(0) is_elastic = (curve_type.value >= QEasingCurve.Type.InElastic.value diff --git a/examples/widgets/dialogs/standarddialogs/standarddialogs.py b/examples/widgets/dialogs/standarddialogs/standarddialogs.py index 7bd68620..541aab30 100644 --- a/examples/widgets/dialogs/standarddialogs/standarddialogs.py +++ b/examples/widgets/dialogs/standarddialogs/standarddialogs.py @@ -8,7 +8,7 @@ from __future__ import annotations import sys from textwrap import dedent -from PySide6.QtCore import QDir, Qt, Slot +from PySide6.QtCore import QDir, QLibraryInfo, QLocale, QTranslator, Qt, Slot from PySide6.QtGui import QFont, QPalette from PySide6.QtWidgets import (QApplication, QColorDialog, QCheckBox, QDialog, QErrorMessage, QFontDialog, QFileDialog, QFrame, @@ -432,6 +432,10 @@ class Dialog(QDialog): if __name__ == '__main__': app = QApplication(sys.argv) + translator = QTranslator(app) + if translator.load(QLocale.system(), "qtbase", "_", + QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)): + app.installTranslator(translator) dialog = Dialog() availableGeometry = dialog.screen().availableGeometry() dialog.resize(availableGeometry.width() / 3, availableGeometry.height() * 2 / 3) diff --git a/examples/widgets/draganddrop/draggabletext/draggabletext.py b/examples/widgets/draganddrop/draggabletext/draggabletext.py index 504e6bd0..b64c499e 100644 --- a/examples/widgets/draganddrop/draggabletext/draggabletext.py +++ b/examples/widgets/draganddrop/draggabletext/draggabletext.py @@ -7,7 +7,7 @@ from __future__ import annotations originating from PyQt""" from PySide6.QtCore import QFile, QIODevice, QMimeData, QPoint, Qt, QTextStream -from PySide6.QtGui import QDrag, QPalette, QPixmap +from PySide6.QtGui import QDrag, QPixmap from PySide6.QtWidgets import QApplication, QFrame, QLabel, QWidget import draggabletext_rc # noqa: F401 @@ -65,10 +65,6 @@ class DragWidget(QWidget): x = 5 y += word_label.height() + 2 - new_palette = self.palette() - new_palette.setColor(QPalette.ColorRole.Window, Qt.GlobalColor.white) - self.setPalette(new_palette) - self.setAcceptDrops(True) self.setMinimumSize(400, max(200, y)) self.setWindowTitle("Draggable Text") diff --git a/examples/widgets/itemviews/rangemodel/doc/rangemodel.rst b/examples/widgets/itemviews/rangemodel/doc/rangemodel.rst new file mode 100644 index 00000000..86176209 --- /dev/null +++ b/examples/widgets/itemviews/rangemodel/doc/rangemodel.rst @@ -0,0 +1,10 @@ +QRangeModel Example +=================== + +A Python application that demonstrates how to populate +a :class:`~PySide6.QtCore.QRangeModel` using +`numpy `_ or Python lists. + +Models created from numpy arrays are editable. + +It requires building PySide6 with the ``--pyside-numpy-support`` option. diff --git a/examples/widgets/itemviews/rangemodel/main.py b/examples/widgets/itemviews/rangemodel/main.py new file mode 100644 index 00000000..485f8a60 --- /dev/null +++ b/examples/widgets/itemviews/rangemodel/main.py @@ -0,0 +1,82 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import numpy +import sys + +from PySide6.QtCore import QRangeModel +from PySide6.QtGui import QKeySequence +from PySide6.QtWidgets import QApplication, QListView, QMainWindow, QTableView, QTabWidget + + +STRING_LIST = ["item1", "item2", "item3", "item4"] +INT_LIST = [1, 2, 3] +INT_TABLE = [[1, 2], [3, 4], [5, 6]] + +NP_INT_ARRAY = numpy.array([1, 2, 3], dtype=numpy.int32) +NP_DOUBLE_ARRAY = numpy.array([1.1, 2.2, 3.3], dtype=numpy.double) + +NP_INT_TABLE = numpy.array([[1, 2, 3], [4, 5, 6]], dtype=numpy.int32) +NP_DOUBLE_TABLE = numpy.array([[1.1, 2.2, 3.3], [4.4, 5.5, 6.6]], dtype=numpy.double) + + +def print_numpy_data(): + print("--------------------------------") + print("NP_INT_ARRAY=", NP_INT_ARRAY) + print("NP_INT_TABLE=", NP_INT_TABLE) + print("NP_DOUBLE_ARRAY=", NP_DOUBLE_ARRAY) + print("NP_DOUBLE_TABLE=", NP_DOUBLE_TABLE) + print("---------------------------------\n") + + +if __name__ == '__main__': + app = QApplication(sys.argv) + window = QMainWindow() + window.setMinimumWidth(600) + file_menu = window.menuBar().addMenu("File") + file_menu.addAction("Output numpy data", print_numpy_data) + file_menu.addAction("Quit", QKeySequence(QKeySequence.StandardKey.Quit), window.close) + help_menu = window.menuBar().addMenu("Help") + help_menu.addAction("About Qt", app.aboutQt) + + tab_widget = QTabWidget() + window.setCentralWidget(tab_widget) + + list_view = QListView() + model = QRangeModel(STRING_LIST) + list_view.setModel(model) + tab_widget.addTab(list_view, "Python String List") + + list_view = QListView() + model = QRangeModel(INT_LIST) + list_view.setModel(model) + tab_widget.addTab(list_view, "Python int List") + + table_view = QTableView() + model = QRangeModel(INT_TABLE) + table_view.setModel(model) + tab_widget.addTab(table_view, "Python Int Table") + + list_view = QListView() + model = QRangeModel(NP_INT_ARRAY) + list_view.setModel(model) + tab_widget.addTab(list_view, "Numpy Int List") + + list_view = QListView() + model = QRangeModel(NP_DOUBLE_ARRAY) + list_view.setModel(model) + tab_widget.addTab(list_view, "Numpy Double List") + + table_view = QTableView() + model = QRangeModel(NP_INT_TABLE) + table_view.setModel(model) + tab_widget.addTab(table_view, "Numpy Int Table") + + table_view = QTableView() + model = QRangeModel(NP_DOUBLE_TABLE) + table_view.setModel(model) + tab_widget.addTab(table_view, "Numpy Double Table") + + window.setWindowTitle("QRangeModel") + window.show() + sys.exit(app.exec()) diff --git a/examples/widgets/itemviews/rangemodel/rangemodel.pyproject b/examples/widgets/itemviews/rangemodel/rangemodel.pyproject new file mode 100644 index 00000000..cc7a74a3 --- /dev/null +++ b/examples/widgets/itemviews/rangemodel/rangemodel.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["main.py"] +} diff --git a/examples/widgets/mainwindows/mdi/mdi.py b/examples/widgets/mainwindows/mdi/mdi.py index 341a7f4d..db3abe34 100644 --- a/examples/widgets/mainwindows/mdi/mdi.py +++ b/examples/widgets/mainwindows/mdi/mdi.py @@ -15,8 +15,6 @@ from PySide6.QtGui import QAction, QIcon, QKeySequence from PySide6.QtWidgets import (QApplication, QFileDialog, QMainWindow, QMdiArea, QMessageBox, QTextEdit) -import PySide6.QtExampleIcons # noqa: F401 - class MdiChild(QTextEdit): sequence_number = 1 @@ -435,10 +433,6 @@ if __name__ == '__main__': app = QApplication(sys.argv) - icon_paths = QIcon.themeSearchPaths() - QIcon.setThemeSearchPaths(icon_paths + [":/qt-project.org/icons"]) - QIcon.setFallbackThemeName("example_icons") - main_win = MainWindow() for f in options.files: main_win.load(f) diff --git a/examples/widgets/widgetsgallery/widgetgallery.py b/examples/widgets/widgetsgallery/widgetgallery.py index 972ffbaa..1f59c157 100644 --- a/examples/widgets/widgetsgallery/widgetgallery.py +++ b/examples/widgets/widgetsgallery/widgetgallery.py @@ -34,7 +34,7 @@ COMPUTER_ICON = ":/qt-project.org/styles/commonstyle/images/computer-32.png" SYSTEMINFO = """

Python

{}

Qt Build

{}

-

Operating System

{}

+

Operating System

"{}" / {}

Screens

{} """ @@ -396,6 +396,7 @@ class WidgetGallery(QDialog): system_info = SYSTEMINFO.format(sys.version, QLibraryInfo.build(), QSysInfo.prettyProductName(), + QGuiApplication.platformName(), screen_info(self)) self._systeminfo_textbrowser.setHtml(system_info) diff --git a/examples/xml/dombookmarks/dombookmarks.py b/examples/xml/dombookmarks/dombookmarks.py index 4f778acb..f0225184 100644 --- a/examples/xml/dombookmarks/dombookmarks.py +++ b/examples/xml/dombookmarks/dombookmarks.py @@ -112,11 +112,12 @@ class XbelTree(QTreeWidget): self._bookmark_icon.addPixmap(style.standardPixmap(QStyle.StandardPixmap.SP_FileIcon)) def read(self, device): - ok, errorStr, errorLine, errorColumn = self._dom_document.setContent(device, True) - if not ok: + result = self._dom_document.setContent(device, + QDomDocument.ParseOption.UseNamespaceProcessing) + if not result: QMessageBox.information(self.window(), "DOM Bookmarks", - f"Parse error at line {errorLine}, " - f"column {errorColumn}:\n{errorStr}") + f"Parse error at line {result.errorLine}, " + f"column {result.errorColumn}:\n{result.errorMessage}") return False root = self._dom_document.documentElement() diff --git a/requirements-doc.txt b/requirements-doc.txt index 7e795c4f..a7d4d544 100644 --- a/requirements-doc.txt +++ b/requirements-doc.txt @@ -1,5 +1,6 @@ sphinx==7.4.7 sphinx-design==0.6.0 +sphinx-collapse sphinx-copybutton==0.5.2 sphinx-tags==0.4 sphinx-toolbox==3.7.0 diff --git a/sources/pyside-tools/cmake/PySideAndroid.cmake b/sources/pyside-tools/cmake/PySideAndroid.cmake index d89da4f1..37b38805 100644 --- a/sources/pyside-tools/cmake/PySideAndroid.cmake +++ b/sources/pyside-tools/cmake/PySideAndroid.cmake @@ -20,10 +20,16 @@ macro(create_and_install_qt_javabindings) ${android_main_srcs}/QtService.java ) # set android.jar from the sdk, for compiling the java files into .jar - set(sdk_jar_location "${ANDROID_SDK_ROOT}/platforms/android-${CMAKE_ANDROID_API}/android.jar") - file(GLOB sources_list LIST_DIRECTORIES true "${ANDROID_SDK_ROOT}/platforms/android-${CMAKE_ANDROID_API}/*") + # Use ANDROID_API_VERSION from environment if set, otherwise fall back to CMAKE_ANDROID_API + if(DEFINED ENV{ANDROID_API_VERSION}) + set(ANDROID_SDK_API_LEVEL "$ENV{ANDROID_API_VERSION}") + else() + set(ANDROID_SDK_API_LEVEL "android-${CMAKE_ANDROID_API}") + endif() + set(sdk_jar_location "${ANDROID_SDK_ROOT}/platforms/${ANDROID_SDK_API_LEVEL}/android.jar") + file(GLOB sources_list LIST_DIRECTORIES true "${ANDROID_SDK_ROOT}/platforms/${ANDROID_SDK_API_LEVEL}/*") if (NOT EXISTS "${sdk_jar_location}") - message(FATAL_ERROR "Could not locate Android SDK jar for api '${CMAKE_ANDROID_API}' - ${sdk_jar_location}") + message(FATAL_ERROR "Could not locate Android SDK jar for api '${ANDROID_SDK_API_LEVEL}' - ${sdk_jar_location}") endif() # this variable is accessed by qt_internal_add_jar diff --git a/sources/pyside-tools/project_lib/pyproject_toml.py b/sources/pyside-tools/project_lib/pyproject_toml.py index fafe0d67..6da7b455 100644 --- a/sources/pyside-tools/project_lib/pyproject_toml.py +++ b/sources/pyside-tools/project_lib/pyproject_toml.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only from __future__ import annotations +import os import sys # TODO: Remove this import when Python 3.11 is the minimum supported version if sys.version_info >= (3, 11): @@ -48,19 +49,19 @@ def _parse_toml_content(content: str) -> dict: return result -def _write_toml_content(data: dict) -> str: +def _write_base_toml_content(data: dict) -> str: """ Write minimal TOML content with project and tool.pyside6-project sections. """ lines = [] - if 'project' in data and data['project']: + if data.get('project'): lines.append('[project]') for key, value in sorted(data['project'].items()): if isinstance(value, str): lines.append(f'{key} = "{value}"') - if 'tool' in data and 'pyside6-project' in data['tool']: + if data.get("tool") and data['tool'].get('pyside6-project'): lines.append('\n[tool.pyside6-project]') for key, value in sorted(data['tool']['pyside6-project'].items()): if isinstance(value, list): @@ -115,7 +116,7 @@ def parse_pyproject_toml(pyproject_toml_file: Path) -> PyProjectParseResult: def write_pyproject_toml(pyproject_file: Path, project_name: str, project_files: list[str]): """ - Create or update a pyproject.toml file with the specified content. + Create or overwrite a pyproject.toml file with the specified content. """ data = { "project": {"name": project_name}, @@ -124,13 +125,33 @@ def write_pyproject_toml(pyproject_file: Path, project_name: str, project_files: } } + content = _write_base_toml_content(data) try: - content = _write_toml_content(data) pyproject_file.write_text(content, encoding='utf-8') except Exception as e: raise ValueError(f"Error writing TOML file: {str(e)}") +def robust_relative_to_posix(target_path: Path, base_path: Path) -> str: + """ + Calculates the relative path from base_path to target_path. + Uses Path.relative_to first, falls back to os.path.relpath if it fails. + Returns the result as a POSIX path string. + """ + # Ensure both paths are absolute for reliable calculation, although in this specific code, + # project_folder and paths in output_files are expected to be resolved/absolute already. + abs_target = target_path.resolve() if not target_path.is_absolute() else target_path + abs_base = base_path.resolve() if not base_path.is_absolute() else base_path + + try: + return abs_target.relative_to(abs_base).as_posix() + except ValueError: + # Fallback to os.path.relpath which is more robust for paths that are not direct subpaths. + relative_str = os.path.relpath(str(abs_target), str(abs_base)) + # Convert back to Path temporarily to get POSIX format + return Path(relative_str).as_posix() + + def migrate_pyproject(pyproject_file: Path | str = None) -> int: """ Migrate a project *.pyproject JSON file to the new pyproject.toml format. @@ -170,7 +191,7 @@ def migrate_pyproject(pyproject_file: Path | str = None) -> int: project_name = project_files[0].stem # The project files that will be written to the pyproject.toml file - output_files = set() + output_files: set[Path] = set() for project_file in project_files: project_data = parse_pyproject_json(project_file) if project_data.errors: @@ -185,39 +206,58 @@ def migrate_pyproject(pyproject_file: Path | str = None) -> int: project_name = project_folder.name pyproject_toml_file = project_folder / "pyproject.toml" - if pyproject_toml_file.exists(): - already_existing_file = True + + relative_files = sorted( + robust_relative_to_posix(p, project_folder) for p in output_files + ) + + if not (already_existing_file := pyproject_toml_file.exists()): + # Create new pyproject.toml file + data = { + "project": {"name": project_name}, + "tool": { + "pyside6-project": {"files": relative_files} + } + } + updated_content = _write_base_toml_content(data) + else: + # For an already existing file, append our tool.pyside6-project section + # If the project section is missing, add it try: content = pyproject_toml_file.read_text(encoding='utf-8') - data = _parse_toml_content(content) except Exception as e: - raise ValueError(f"Error parsing TOML: {str(e)}") - else: - already_existing_file = False - data = {"project": {}, "tool": {"pyside6-project": {}}} + print(f"Error processing existing TOML file: {str(e)}", file=sys.stderr) + return 1 - # Update project name if not present - if "name" not in data["project"]: - data["project"]["name"] = project_name + append_content = [] - # Update files list - data["tool"]["pyside6-project"]["files"] = sorted( - p.relative_to(project_folder).as_posix() for p in output_files - ) + if '[project]' not in content: + # Add project section if needed + append_content.append('\n[project]') + append_content.append(f'name = "{project_name}"') + + if '[tool.pyside6-project]' not in content: + # Add tool.pyside6-project section + append_content.append('\n[tool.pyside6-project]') + items = [f'"{item}"' for item in relative_files] + append_content.append(f'files = [{", ".join(items)}]') - # Generate TOML content - toml_content = _write_toml_content(data) + if append_content: + updated_content = content.rstrip() + '\n' + '\n'.join(append_content) + else: + # No changes needed + print("pyproject.toml already contains [project] and [tool.pyside6-project] sections") + return 0 - if already_existing_file: print(f"WARNING: A pyproject.toml file already exists at \"{pyproject_toml_file}\"") print("The file will be updated with the following content:") - print(toml_content) + print(updated_content) response = input("Proceed? [Y/n] ") if response.lower().strip() not in {"yes", "y"}: return 0 try: - pyproject_toml_file.write_text(toml_content) + pyproject_toml_file.write_text(updated_content, encoding='utf-8') except Exception as e: print(f"Error writing to \"{pyproject_toml_file}\": {str(e)}", file=sys.stderr) return 1 diff --git a/sources/pyside6/.cmake.conf b/sources/pyside6/.cmake.conf index f1458e93..ffe43a7b 100644 --- a/sources/pyside6/.cmake.conf +++ b/sources/pyside6/.cmake.conf @@ -1,5 +1,5 @@ set(pyside_MAJOR_VERSION "6") -set(pyside_MINOR_VERSION "9") +set(pyside_MINOR_VERSION "10") set(pyside_MICRO_VERSION "2") set(pyside_PRE_RELEASE_VERSION_TYPE "") set(pyside_PRE_RELEASE_VERSION "") diff --git a/sources/pyside6/CMakeLists.txt b/sources/pyside6/CMakeLists.txt index 9c238e98..6b73d70a 100644 --- a/sources/pyside6/CMakeLists.txt +++ b/sources/pyside6/CMakeLists.txt @@ -9,6 +9,8 @@ cmake_policy(VERSION 3.18) include(".cmake.conf") project(pysidebindings) +set(QT_NO_PRIVATE_MODULE_WARNING ON) + include(cmake/PySideSetup.cmake) get_rpath_base_token(base) @@ -29,6 +31,19 @@ if(Qt${QT_MAJOR_VERSION}RemoteObjects_FOUND) add_subdirectory(libpysideremoteobjects) endif() +# build-time export set for PySide6 full build +install(EXPORT PySide6Targets + NAMESPACE PySide6:: + DESTINATION "${LIB_INSTALL_DIR}/cmake/PySide6") + +# wheel export set +if(NOT is_pyside6_superproject_build) + install(EXPORT PySide6WheelTargets + NAMESPACE PySide6:: + DESTINATION "${LIB_INSTALL_DIR}/wheels/cmake/PySide6" + FILE PySide6Targets.cmake) +endif() + if(Qt${QT_MAJOR_VERSION}UiTools_FOUND) add_subdirectory(plugins/uitools) find_package(Qt6 COMPONENTS Designer) @@ -45,5 +60,3 @@ if(BUILD_TESTS) endif() add_subdirectory(doc) - -add_subdirectory(qtexampleicons) diff --git a/sources/pyside6/PySide6/CMakeLists.txt b/sources/pyside6/PySide6/CMakeLists.txt index e5b0b672..c1934d9c 100644 --- a/sources/pyside6/PySide6/CMakeLists.txt +++ b/sources/pyside6/PySide6/CMakeLists.txt @@ -4,9 +4,6 @@ project(pyside6) # Configure include based on platform -configure_file("${CMAKE_CURRENT_SOURCE_DIR}/global.h.in" - "${CMAKE_CURRENT_BINARY_DIR}/pyside6_global.h" @ONLY) - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/__init__.py.in" "${CMAKE_CURRENT_BINARY_DIR}/__init__.py" @ONLY) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/_config.py.in" @@ -50,8 +47,24 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/support/generate_pyi.py" configure_file("${CMAKE_CURRENT_SOURCE_DIR}/support/deprecated.py" "${CMAKE_CURRENT_BINARY_DIR}/support/deprecated.py" COPYONLY) +# Additional (non-Qt) modules implemented in PySide only +foreach(mod IN LISTS PURE_PYTHON_MODULES) + set(src_dir "${CMAKE_CURRENT_SOURCE_DIR}/Qt${mod}") + set(dst_dir "${CMAKE_CURRENT_BINARY_DIR}/Qt${mod}") + + if(EXISTS "${src_dir}") + file(GLOB_RECURSE module_files "${src_dir}/*") + + foreach(f ${module_files}) + file(RELATIVE_PATH relpath "${src_dir}" "${f}") + set(dst "${dst_dir}/${relpath}") + configure_file("${f}" "${dst}" COPYONLY) + endforeach() + endif() +endforeach() + # now compile all modules. -file(READ "${CMAKE_CURRENT_BINARY_DIR}/pyside6_global.h" pyside6_global_contents) +file(READ "${CMAKE_CURRENT_LIST_DIR}/pyside6_global.h" pyside6_global_contents) foreach(shortname IN LISTS all_module_shortnames) set(name "Qt${QT_MAJOR_VERSION}${shortname}") @@ -105,8 +118,8 @@ install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/templates/widgets_common.xml DESTINATION share/PySide6${pyside_SUFFIX}/typesystems) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/templates/datavisualization_common.xml DESTINATION share/PySide6${pyside_SUFFIX}/typesystems) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pyside6_global.h - DESTINATION include/${BINDING_NAME}${pyside6_SUFFIX}) +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/pyside6_global.h + DESTINATION ${BINDING_NAME}${pyside6_SUFFIX}/include) install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/QtAsyncio" DESTINATION "${PYTHON_SITE_PACKAGES}/${BINDING_NAME}${pyside6_SUFFIX}") diff --git a/sources/pyside6/PySide6/QtAsyncio/events.py b/sources/pyside6/PySide6/QtAsyncio/events.py index 65f3ccbc..6e208845 100644 --- a/sources/pyside6/PySide6/QtAsyncio/events.py +++ b/sources/pyside6/PySide6/QtAsyncio/events.py @@ -8,7 +8,7 @@ from PySide6.QtCore import (QCoreApplication, QDateTime, QDeadlineTimer, from . import futures from . import tasks -from typing import Any, Callable +from typing import Any, Callable, TypeVar import asyncio import collections.abc @@ -26,6 +26,22 @@ __all__ = [ "QAsyncioHandle", "QAsyncioTimerHandle", ] +from typing import TYPE_CHECKING + +_T = TypeVar("_T") + +if TYPE_CHECKING: + try: + from typing import TypeVarTuple, Unpack + except ImportError: + from typing_extensions import TypeVarTuple, Unpack # type: ignore + + _Ts = TypeVarTuple("_Ts") + Context = contextvars.Context # type: ignore +else: + _Ts = None # type: ignore + Context = contextvars.Context + class QAsyncioExecutorWrapper(QObject): """ @@ -41,14 +57,13 @@ class QAsyncioExecutorWrapper(QObject): the executor thread, and then creates a zero-delay singleshot timer to push the actual callable for the executor into this new event loop. """ - - def __init__(self, func: Callable, *args: tuple) -> None: + def __init__(self, func: Callable[[Unpack[_Ts]], Any], *args: Unpack[_Ts]) -> None: super().__init__() self._loop: QEventLoop self._func = func self._args = args - self._result = None - self._exception = None + self._result: Any = None + self._exception: BaseException | None = None def _cb(self): try: @@ -59,7 +74,7 @@ class QAsyncioExecutorWrapper(QObject): self._exception = e self._loop.exit() - def do(self): + def do(self) -> Any: # This creates a new event loop and dispatcher for the thread, if not # already created. self._loop = QEventLoop() @@ -326,7 +341,7 @@ class QAsyncioEventLoop(asyncio.BaseEventLoop, QObject): if timeout is not None: deadline_timer = QDeadlineTimer(int(timeout * 1000)) else: - deadline_timer = QDeadlineTimer(QDeadlineTimer.Forever) + deadline_timer = QDeadlineTimer(QDeadlineTimer.ForeverConstant.Forever) if self._default_executor is None: return @@ -346,50 +361,50 @@ class QAsyncioEventLoop(asyncio.BaseEventLoop, QObject): # Scheduling callbacks - def _call_soon_impl(self, callback: Callable, *args: Any, - context: contextvars.Context | None = None, + def _call_soon_impl(self, callback: Callable[[Unpack[_Ts]], object], *args: Unpack[_Ts], + context: Context | None = None, is_threadsafe: bool | None = False) -> asyncio.Handle: return self._call_later_impl(0, callback, *args, context=context, is_threadsafe=is_threadsafe) - def call_soon(self, callback: Callable, *args: Any, - context: contextvars.Context | None = None) -> asyncio.Handle: + def call_soon(self, callback: Callable[[Unpack[_Ts]], object], *args: Unpack[_Ts], + context: Context | None = None) -> asyncio.Handle: return self._call_soon_impl(callback, *args, context=context, is_threadsafe=False) - def call_soon_threadsafe(self, callback: Callable, *args: Any, - context: contextvars.Context | None = None) -> asyncio.Handle: + def call_soon_threadsafe(self, callback: Callable[[Unpack[_Ts]], object], *args: Unpack[_Ts], + context: Context | None = None) -> asyncio.Handle: if self.is_closed(): raise RuntimeError("Event loop is closed") if context is None: context = contextvars.copy_context() return self._call_soon_impl(callback, *args, context=context, is_threadsafe=True) - def _call_later_impl(self, delay: int | float, callback: Callable, *args: Any, - context: contextvars.Context | None = None, + def _call_later_impl(self, delay: float, callback: Callable[[Unpack[_Ts]], object], + *args: Unpack[_Ts], context: Context | None = None, is_threadsafe: bool | None = False) -> asyncio.TimerHandle: if not isinstance(delay, (int, float)): raise TypeError("delay must be an int or float") - return self._call_at_impl(self.time() + delay, callback, *args, context=context, - is_threadsafe=is_threadsafe) + return self._call_at_impl(self.time() + delay, callback, *args, + context=context, is_threadsafe=is_threadsafe) - def call_later(self, delay: int | float, callback: Callable, *args: Any, - context: contextvars.Context | None = None) -> asyncio.TimerHandle: + def call_later(self, delay: float, callback: Callable[[Unpack[_Ts]], object], + *args: Unpack[_Ts], context: Context | None = None) -> asyncio.TimerHandle: return self._call_later_impl(delay, callback, *args, context=context, is_threadsafe=False) - def _call_at_impl(self, when: int | float, callback: Callable, *args: Any, - context: contextvars.Context | None = None, + def _call_at_impl(self, when: float, callback: Callable[[Unpack[_Ts]], object], + *args: Unpack[_Ts], context: Context | None = None, is_threadsafe: bool | None = False) -> asyncio.TimerHandle: """ All call_at() and call_later() methods map to this method. """ if not isinstance(when, (int, float)): raise TypeError("when must be an int or float") return QAsyncioTimerHandle(when, callback, args, self, context, is_threadsafe=is_threadsafe) - def call_at(self, when: int | float, callback: Callable, *args: Any, - context: contextvars.Context | None = None) -> asyncio.TimerHandle: + def call_at(self, when: float, callback: Callable[[Unpack[_Ts]], object], + *args: Unpack[_Ts], context: Context | None = None) -> asyncio.TimerHandle: return self._call_at_impl(when, callback, *args, context=context, is_threadsafe=False) def time(self) -> float: - return QDateTime.currentMSecsSinceEpoch() / 1000 + return QDateTime.currentMSecsSinceEpoch() / 1000.0 # Creating Futures and Tasks @@ -560,9 +575,9 @@ class QAsyncioEventLoop(asyncio.BaseEventLoop, QObject): # Executing code in thread or process pools - def run_in_executor(self, - executor: concurrent.futures.ThreadPoolExecutor | None, - func: Callable, *args: tuple) -> asyncio.futures.Future: + def run_in_executor(self, executor: concurrent.futures.ThreadPoolExecutor | None, + func: Callable[[Unpack[_Ts]], _T], + *args: Unpack[_Ts]) -> asyncio.Future[_T]: if self.is_closed(): raise RuntimeError("Event loop is closed") if executor is None: @@ -575,9 +590,7 @@ class QAsyncioEventLoop(asyncio.BaseEventLoop, QObject): # attaches a QEventLoop to the executor thread, and then pushes the # actual callable for the executor into this new event loop. wrapper = QAsyncioExecutorWrapper(func, *args) - return asyncio.futures.wrap_future( - executor.submit(wrapper.do), loop=self - ) + return asyncio.futures.wrap_future(executor.submit(wrapper.do), loop=self) def set_default_executor(self, executor: concurrent.futures.ThreadPoolExecutor | None) -> None: @@ -649,13 +662,11 @@ class QAsyncioHandle(): loop: QAsyncioEventLoop, context: contextvars.Context | None, is_threadsafe: bool | None = False) -> None: self._callback = callback - self._args = args + self._cb_args = args # renamed from _args to avoid conflict with TimerHandle._args self._loop = loop self._context = context self._is_threadsafe = is_threadsafe - self._timeout = 0 - self._state = QAsyncioHandle.HandleState.PENDING self._start() @@ -685,9 +696,9 @@ class QAsyncioHandle(): """ if self._state == QAsyncioHandle.HandleState.PENDING: if self._context is not None: - self._context.run(self._callback, *self._args) + self._context.run(self._callback, *self._cb_args) else: - self._callback(*self._args) + self._callback(*self._cb_args) self._state = QAsyncioHandle.HandleState.DONE def cancel(self) -> None: diff --git a/sources/pyside6/PySide6/QtAsyncio/tasks.py b/sources/pyside6/PySide6/QtAsyncio/tasks.py index deabf690..c6cb3a10 100644 --- a/sources/pyside6/PySide6/QtAsyncio/tasks.py +++ b/sources/pyside6/PySide6/QtAsyncio/tasks.py @@ -6,7 +6,7 @@ from . import events from . import futures import traceback -from typing import Any +from typing import Any, Optional import asyncio import collections.abc @@ -21,6 +21,10 @@ class QAsyncioTask(futures.QAsyncioFuture): loop: "events.QAsyncioEventLoop | None" = None, name: str | None = None, context: contextvars.Context | None = None) -> None: super().__init__(loop=loop, context=context) + self._source_traceback = None # required for Python < 3.11 + + self._state: futures.QAsyncioFuture.FutureState = futures.QAsyncioFuture.FutureState.PENDING + self._exception: Optional[BaseException] = None self._coro = coro # The coroutine for which this task was created. self._name = name if name else "QtTask" @@ -40,12 +44,13 @@ class QAsyncioTask(futures.QAsyncioFuture): self._cancel_count = 0 self._cancel_message: str | None = None # Store traceback in case of Exception. Useful when exception happens in coroutine - self._tb: str = None + self._tb: str | None = None # https://docs.python.org/3/library/asyncio-extending.html#task-lifetime-support asyncio._register_task(self) # type: ignore[arg-type] def __repr__(self) -> str: + state: str = "Unknown" if self._state == futures.QAsyncioFuture.FutureState.PENDING: state = "Pending" elif self._state == futures.QAsyncioFuture.FutureState.DONE_WITH_RESULT: diff --git a/sources/pyside6/PySide6/QtCore/CMakeLists.txt b/sources/pyside6/PySide6/QtCore/CMakeLists.txt index d559f9d9..2f49b610 100644 --- a/sources/pyside6/PySide6/QtCore/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtCore/CMakeLists.txt @@ -15,13 +15,10 @@ set(QtCore_static_sources "${pyside6_SOURCE_DIR}/qiopipe.h" ) -if(ENABLE_WIN) +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") set(SPECIFIC_OS_FILES ${QtCore_GEN_DIR}/qwineventnotifier_wrapper.cpp ) -else() - set(SPECIFIC_OS_FILES - ${QtCore_GEN_DIR}/qprocess_unixprocessparameters_wrapper.cpp) endif() set(QtCore_SRC @@ -82,6 +79,7 @@ ${QtCore_GEN_DIR}/qfileselector_wrapper.cpp ${QtCore_GEN_DIR}/qfilesystemwatcher_wrapper.cpp ${QtCore_GEN_DIR}/qfutureinterfacebase_wrapper.cpp ${QtCore_GEN_DIR}/qgenericargument_wrapper.cpp +${QtCore_GEN_DIR}/qrangemodel_wrapper.cpp ${QtCore_GEN_DIR}/qgenericreturnargument_wrapper.cpp ${QtCore_GEN_DIR}/qhashseed_wrapper.cpp ${QtCore_GEN_DIR}/qidentityproxymodel_wrapper.cpp @@ -130,8 +128,6 @@ ${QtCore_GEN_DIR}/qpersistentmodelindex_wrapper.cpp ${QtCore_GEN_DIR}/qpluginloader_wrapper.cpp ${QtCore_GEN_DIR}/qpoint_wrapper.cpp ${QtCore_GEN_DIR}/qpointf_wrapper.cpp -${QtCore_GEN_DIR}/qprocess_wrapper.cpp -${QtCore_GEN_DIR}/qprocessenvironment_wrapper.cpp ${QtCore_GEN_DIR}/qpropertyanimation_wrapper.cpp ${QtCore_GEN_DIR}/qrandomgenerator64_wrapper.cpp ${QtCore_GEN_DIR}/qrandomgenerator_wrapper.cpp @@ -236,6 +232,23 @@ else() list(APPEND QtCore_SRC ${QtCore_GEN_DIR}/qsharedmemory_wrapper.cpp) endif() +if("process" IN_LIST QtCore_disabled_features) + list(APPEND QtCore_DROPPED_ENTRIES QProcess) + message(STATUS "Qt${QT_MAJOR_VERSION}Core: Dropping QProcess") +else() + list(APPEND QtCore_SRC ${QtCore_GEN_DIR}/qprocess_wrapper.cpp) + if(NOT CMAKE_SYSTEM_NAME STREQUAL "Windows") + list(APPEND QtCore_SRC ${QtCore_GEN_DIR}/qprocess_unixprocessparameters_wrapper.cpp) + endif() +endif() + +if("processenvironment" IN_LIST QtCore_disabled_features) + list(APPEND QtCore_DROPPED_ENTRIES QProcessEnvironment) + message(STATUS "Qt${QT_MAJOR_VERSION}Core: Dropping QProcessEnvironment") +else() + list(APPEND QtCore_SRC ${QtCore_GEN_DIR}/qprocessenvironment_wrapper.cpp) +endif() + configure_file("${QtCore_SOURCE_DIR}/QtCore_global.post.h.in" "${QtCore_BINARY_DIR}/QtCore_global.post.h" @ONLY) @@ -293,4 +306,4 @@ if (APPLE) endif() install(FILES ${pyside6_SOURCE_DIR}/qtcorehelper.h ${pyside6_SOURCE_DIR}/qiopipe.h - DESTINATION include/PySide6/QtCore/) + DESTINATION PySide6/include/QtCore/) diff --git a/sources/pyside6/PySide6/QtCore/glue/core_snippets.cpp b/sources/pyside6/PySide6/QtCore/glue/core_snippets.cpp index c51d2274..a77a8dde 100644 --- a/sources/pyside6/PySide6/QtCore/glue/core_snippets.cpp +++ b/sources/pyside6/PySide6/QtCore/glue/core_snippets.cpp @@ -5,7 +5,9 @@ #include "qtcorehelper.h" #include "pysideqobject.h" -#include "shiboken.h" +#include "sbkpython.h" +#include "sbkconverter.h" +#include "sbkpep.h" #ifndef Py_LIMITED_API # include #endif @@ -19,111 +21,8 @@ #include #include #include -#include -// Helpers for QVariant conversion - -QMetaType QVariant_resolveMetaType(PyTypeObject *type) -{ - if (!PyObject_TypeCheck(type, SbkObjectType_TypeF())) - return {}; - const char *typeName = Shiboken::ObjectType::getOriginalName(type); - if (!typeName) - return {}; - const bool valueType = '*' != typeName[qstrlen(typeName) - 1]; - // Do not convert user type of value - if (valueType && Shiboken::ObjectType::isUserType(type)) - return {}; - QMetaType metaType = QMetaType::fromName(typeName); - if (metaType.isValid()) - return metaType; - // Do not resolve types to value type - if (valueType) - return {}; - // Find in base types. First check tp_bases, and only after check tp_base, because - // tp_base does not always point to the first base class, but rather to the first - // that has added any python fields or slots to its object layout. - // See https://mail.python.org/pipermail/python-list/2009-January/520733.html - if (type->tp_bases) { - const auto size = PyTuple_Size(type->tp_bases); - Py_ssize_t i = 0; - // PYSIDE-1887, PYSIDE-86: Skip QObject base class of QGraphicsObject; - // it needs to use always QGraphicsItem as a QVariant type for - // QGraphicsItem::itemChange() to work. - if (qstrcmp(typeName, "QGraphicsObject*") == 0 && size > 1) { - auto *firstBaseType = reinterpret_cast(PyTuple_GetItem(type->tp_bases, 0)); - if (SbkObjectType_Check(firstBaseType)) { - const char *firstBaseTypeName = Shiboken::ObjectType::getOriginalName(firstBaseType); - if (firstBaseTypeName != nullptr && qstrcmp(firstBaseTypeName, "QObject*") == 0) - ++i; - } - } - for ( ; i < size; ++i) { - auto baseType = reinterpret_cast(PyTuple_GetItem(type->tp_bases, i)); - const QMetaType derived = QVariant_resolveMetaType(baseType); - if (derived.isValid()) - return derived; - } - } else if (type->tp_base) { - return QVariant_resolveMetaType(type->tp_base); - } - return {}; -} - -QVariant QVariant_convertToValueList(PyObject *list) -{ - if (PySequence_Size(list) < 0) { - // clear the error if < 0 which means no length at all - PyErr_Clear(); - return {}; - } - - Shiboken::AutoDecRef element(PySequence_GetItem(list, 0)); - - auto *type = reinterpret_cast(element.object()); - QMetaType metaType = QVariant_resolveMetaType(type); - if (!metaType.isValid()) - return {}; - - const QByteArray listTypeName = QByteArrayLiteral("QList<") + metaType.name() + '>'; - metaType = QMetaType::fromName(listTypeName); - if (!metaType.isValid()) - return {}; - - Shiboken::Conversions::SpecificConverter converter(listTypeName); - if (!converter) { - qWarning("Type converter for: %s not registered.", listTypeName.constData()); - return {}; - } - - QVariant var(metaType); - converter.toCpp(list, &var); - return var; -} - -bool QVariant_isStringList(PyObject *list) -{ - if (!PySequence_Check(list)) { - // If it is not a list or a derived list class - // we assume that will not be a String list neither. - return false; - } - - if (PySequence_Size(list) < 0) { - // clear the error if < 0 which means no length at all - PyErr_Clear(); - return false; - } - - Shiboken::AutoDecRef fast(PySequence_Fast(list, "Failed to convert QVariantList")); - const Py_ssize_t size = PySequence_Size(fast.object()); - for (Py_ssize_t i = 0; i < size; ++i) { - Shiboken::AutoDecRef item(PySequence_GetItem(fast.object(), i)); - if (PyUnicode_Check(item) == 0) - return false; - } - return true; -} +#include // Helpers for qAddPostRoutine @@ -246,10 +145,7 @@ QString qObjectTr(PyTypeObject *type, const char *sourceText, const char *disamb auto *type = reinterpret_cast(PyTuple_GetItem(mro, idx)); if (type == sbkObjectType) continue; - const char *context = type->tp_name; - const char *dotpos = strrchr(context, '.'); - if (dotpos != nullptr) - context = dotpos + 1; + const char *context = PepType_GetNameStr(type); result = QCoreApplication::translate(context, sourceText, disambiguation, n); if (result != oldResult) break; diff --git a/sources/pyside6/PySide6/QtCore/glue/core_snippets_p.h b/sources/pyside6/PySide6/QtCore/glue/core_snippets_p.h index 11e84b29..4c1867a1 100644 --- a/sources/pyside6/PySide6/QtCore/glue/core_snippets_p.h +++ b/sources/pyside6/PySide6/QtCore/glue/core_snippets_p.h @@ -14,10 +14,8 @@ QT_FORWARD_DECLARE_CLASS(QGenericArgument) QT_FORWARD_DECLARE_CLASS(QGenericReturnArgument) -QT_FORWARD_DECLARE_CLASS(QMetaType) QT_FORWARD_DECLARE_CLASS(QObject) QT_FORWARD_DECLARE_CLASS(QRegularExpression) -QT_FORWARD_DECLARE_CLASS(QVariant); QT_BEGIN_NAMESPACE namespace QtCoreHelper { @@ -26,14 +24,6 @@ class QGenericReturnArgumentHolder; } QT_END_NAMESPACE -// Helpers for QVariant conversion - -QMetaType QVariant_resolveMetaType(PyTypeObject *type); - -QVariant QVariant_convertToValueList(PyObject *list); - -bool QVariant_isStringList(PyObject *list); - // Helpers for qAddPostRoutine namespace PySide { void globalPostRoutineCallback(); diff --git a/sources/pyside6/PySide6/QtCore/glue/qeasingcurve_glue.cpp b/sources/pyside6/PySide6/QtCore/glue/qeasingcurve_glue.cpp index d806b23e..3ff157e4 100644 --- a/sources/pyside6/PySide6/QtCore/glue/qeasingcurve_glue.cpp +++ b/sources/pyside6/PySide6/QtCore/glue/qeasingcurve_glue.cpp @@ -2,7 +2,8 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include -#include +#include +#include #include #include diff --git a/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml b/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml index 1544e6d4..3be80d31 100644 --- a/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml +++ b/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml @@ -16,7 +16,9 @@ + + @@ -318,6 +320,8 @@ + + @@ -328,6 +332,7 @@ + @@ -388,6 +393,9 @@ + + + @@ -613,6 +621,7 @@ + @@ -770,6 +779,7 @@ + @@ -799,6 +809,7 @@ + @@ -931,9 +942,12 @@ - + + + + @@ -959,6 +973,14 @@ + + + + + + + + @@ -1055,6 +1077,7 @@ + @@ -1088,6 +1111,7 @@ + @@ -1251,6 +1275,7 @@ + @@ -1284,6 +1309,7 @@ + @@ -1718,6 +1744,33 @@ + + + + + + + + + + + + + + + + + + + + + @@ -3155,6 +3212,7 @@ + @@ -3353,6 +3411,9 @@ + + + @@ -3388,6 +3449,10 @@ + + + @@ -3395,9 +3460,8 @@ - - - + @@ -3406,9 +3470,8 @@ - - - + diff --git a/sources/pyside6/PySide6/QtDBus/CMakeLists.txt b/sources/pyside6/PySide6/QtDBus/CMakeLists.txt index 5ee8dc67..49e86d12 100644 --- a/sources/pyside6/PySide6/QtDBus/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtDBus/CMakeLists.txt @@ -51,4 +51,4 @@ create_pyside_module(NAME QtDBus TYPESYSTEM_PATH QtDBus_SOURCE_DIR SOURCES QtDBus_SRC) -install(FILES ${pyside6_SOURCE_DIR}/qtdbushelper.h DESTINATION include/PySide6/QtDBus) +install(FILES ${pyside6_SOURCE_DIR}/qtdbushelper.h DESTINATION PySide6/include/QtDBus) diff --git a/sources/pyside6/PySide6/QtDataVisualization/CMakeLists.txt b/sources/pyside6/PySide6/QtDataVisualization/CMakeLists.txt index 1276b424..424799b2 100644 --- a/sources/pyside6/PySide6/QtDataVisualization/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtDataVisualization/CMakeLists.txt @@ -68,4 +68,4 @@ create_pyside_module(NAME QtDataVisualization STATIC_SOURCES QtDataVisualization_src) install(FILES ${pyside6_SOURCE_DIR}/qtdatavisualization_helper.h - DESTINATION include/PySide6/QtDataVisualization) + DESTINATION PySide6/include/QtDataVisualization) diff --git a/sources/pyside6/PySide6/QtDesigner/CMakeLists.txt b/sources/pyside6/PySide6/QtDesigner/CMakeLists.txt index e91532b8..570ed0f0 100644 --- a/sources/pyside6/PySide6/QtDesigner/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtDesigner/CMakeLists.txt @@ -67,4 +67,4 @@ create_pyside_module(NAME QtDesigner STATIC_SOURCES QtDesigner_static_src TYPESYSTEM_NAME ${QtDesigner_BINARY_DIR}/typesystem_designer.xml) -install(FILES ${pyside6_SOURCE_DIR}/qpydesignerextensions.h DESTINATION include/PySide6/QtDesigner) +install(FILES ${pyside6_SOURCE_DIR}/qpydesignerextensions.h DESTINATION PySide6/include/QtDesigner) diff --git a/sources/pyside6/PySide6/QtDesigner/qpydesignercustomwidgetcollection.cpp b/sources/pyside6/PySide6/QtDesigner/qpydesignercustomwidgetcollection.cpp index 19e97423..d93c038b 100644 --- a/sources/pyside6/PySide6/QtDesigner/qpydesignercustomwidgetcollection.cpp +++ b/sources/pyside6/PySide6/QtDesigner/qpydesignercustomwidgetcollection.cpp @@ -6,8 +6,12 @@ #include #include -#include +#include +#include +#include #include +#include +#include QT_BEGIN_NAMESPACE @@ -117,19 +121,20 @@ QWidget *PyDesignerCustomWidget::createWidget(QWidget *parent) PyTuple_SetItem(pyArgs, 0, pyParent); // tuple will keep pyParent reference // Call python constructor - auto result = reinterpret_cast(PyObject_CallObject(m_pyTypeObject, pyArgs)); - if (!result) { + auto *obResult = PyObject_CallObject(m_pyTypeObject, pyArgs); + if (obResult == nullptr) { qWarning("Unable to create a Python custom widget of type \"%s\".", utf8Name()); PyErr_Print(); return nullptr; } + auto *result = reinterpret_cast(obResult); if (unknownParent) // if parent does not exist in python, transfer the ownership to cpp Shiboken::Object::releaseOwnership(result); else - Shiboken::Object::setParent(pyParent, reinterpret_cast(result)); + Shiboken::Object::setParent(pyParent, obResult); - return reinterpret_cast(Shiboken::Object::cppPointer(result, Py_TYPE(result))); + return reinterpret_cast(Shiboken::Object::cppPointer(result, Py_TYPE(obResult))); } void PyDesignerCustomWidget::initialize(QDesignerFormEditorInterface *core) diff --git a/sources/pyside6/PySide6/QtGraphs/CMakeLists.txt b/sources/pyside6/PySide6/QtGraphs/CMakeLists.txt index 29a7b254..f4160787 100644 --- a/sources/pyside6/PySide6/QtGraphs/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtGraphs/CMakeLists.txt @@ -96,4 +96,4 @@ create_pyside_module(NAME QtGraphs DROPPED_ENTRIES QtGraphs_DROPPED_ENTRIES) install(FILES ${pyside6_SOURCE_DIR}/qtgraphs_helper.h - DESTINATION include/PySide6/QtGraphs) + DESTINATION PySide6/include/QtGraphs) diff --git a/sources/pyside6/PySide6/QtGraphs/typesystem_graphs.xml b/sources/pyside6/PySide6/QtGraphs/typesystem_graphs.xml index 81485a70..ba0a8a48 100644 --- a/sources/pyside6/PySide6/QtGraphs/typesystem_graphs.xml +++ b/sources/pyside6/PySide6/QtGraphs/typesystem_graphs.xml @@ -23,6 +23,7 @@ + @@ -54,6 +55,7 @@ + @@ -98,7 +100,9 @@ - + + + diff --git a/sources/pyside6/PySide6/QtGui/CMakeLists.txt b/sources/pyside6/PySide6/QtGui/CMakeLists.txt index e3207856..b9e84291 100644 --- a/sources/pyside6/PySide6/QtGui/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtGui/CMakeLists.txt @@ -19,6 +19,7 @@ set_property(SOURCE ${QtGui_SRC_UNITY_EXCLUDED_SRC} set(QtGui_SRC_RHI ${QtGui_GEN_DIR}/qrhi_wrapper.cpp +${QtGui_GEN_DIR}/qrhiadapter_wrapper.cpp ${QtGui_GEN_DIR}/qrhibuffer_wrapper.cpp ${QtGui_GEN_DIR}/qrhicolorattachment_wrapper.cpp ${QtGui_GEN_DIR}/qrhicommandbuffer_wrapper.cpp @@ -67,13 +68,13 @@ ${QtGui_GEN_DIR}/qshadercode_wrapper.cpp ${QtGui_GEN_DIR}/qshaderkey_wrapper.cpp ) -if (ENABLE_WIN) +if (CMAKE_SYSTEM_NAME STREQUAL "Windows") list(APPEND QtGui_SRC_RHI ${QtGui_GEN_DIR}/qrhid3d11initparams_wrapper.cpp ${QtGui_GEN_DIR}/qrhid3d11nativehandles_wrapper.cpp ${QtGui_GEN_DIR}/qrhid3d12initparams_wrapper.cpp ${QtGui_GEN_DIR}/qrhid3d12nativehandles_wrapper.cpp) -elseif (ENABLE_MAC) +elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin") list(APPEND QtGui_SRC_RHI ${QtGui_GEN_DIR}/qrhimetalinitparams_wrapper.cpp) endif() @@ -84,6 +85,7 @@ ${QtGui_GEN_DIR}/qabstractfileiconprovider_wrapper.cpp ${QtGui_GEN_DIR}/qabstracttextdocumentlayout_paintcontext_wrapper.cpp ${QtGui_GEN_DIR}/qabstracttextdocumentlayout_selection_wrapper.cpp ${QtGui_GEN_DIR}/qabstracttextdocumentlayout_wrapper.cpp +${QtGui_GEN_DIR}/qaccessibilityhints_wrapper.cpp ${QtGui_GEN_DIR}/qaccessible_wrapper.cpp ${QtGui_GEN_DIR}/qaccessibleactioninterface_wrapper.cpp ${QtGui_GEN_DIR}/qaccessibleannouncementevent_wrapper.cpp @@ -294,7 +296,14 @@ get_property(QtGui_enabled_features TARGET Qt${QT_MAJOR_VERSION}::Gui if("xcb" IN_LIST QtGui_enabled_features) list(APPEND QtGui_SRC ${QtGui_GEN_DIR}/qnativeinterface_qx11application_wrapper.cpp) -elseif(WIN32) +endif() + +if("wayland" IN_LIST QtGui_enabled_features) + list(APPEND QtGui_SRC + ${QtGui_GEN_DIR}/qnativeinterface_qwaylandapplication_wrapper.cpp) +endif() + +if(WIN32) list(APPEND QtGui_SRC ${QtGui_GEN_DIR}/qnativeinterface_qwindowsscreen_wrapper.cpp) endif() @@ -357,4 +366,4 @@ create_pyside_module(NAME QtGui install(FILES ${pyside6_SOURCE_DIR}/qpytextobject.h ${pyside6_SOURCE_DIR}/qtguihelper.h - DESTINATION include/PySide6/QtGui/) + DESTINATION PySide6/include/QtGui/) diff --git a/sources/pyside6/PySide6/QtGui/typesystem_gui.xml b/sources/pyside6/PySide6/QtGui/typesystem_gui.xml index 85898940..74f298cd 100644 --- a/sources/pyside6/PySide6/QtGui/typesystem_gui.xml +++ b/sources/pyside6/PySide6/QtGui/typesystem_gui.xml @@ -14,7 +14,9 @@ + + diff --git a/sources/pyside6/PySide6/QtGui/typesystem_gui_common.xml b/sources/pyside6/PySide6/QtGui/typesystem_gui_common.xml index 5cf69940..ae298a8f 100644 --- a/sources/pyside6/PySide6/QtGui/typesystem_gui_common.xml +++ b/sources/pyside6/PySide6/QtGui/typesystem_gui_common.xml @@ -10,6 +10,11 @@ + + + + + @@ -120,6 +125,7 @@ + @@ -474,10 +480,10 @@ - + - + @@ -486,10 +492,20 @@ + + + + - + + + + + + @@ -1655,26 +1671,6 @@ - - - - - - - - - - - - - - - - - - - - @@ -1735,12 +1731,6 @@ - - - - - - @@ -2251,6 +2241,9 @@ + + + @@ -2288,6 +2281,9 @@ + + + @@ -2325,6 +2321,9 @@ + + + @@ -2362,6 +2361,9 @@ + + + @@ -2399,6 +2401,9 @@ + + + @@ -2436,6 +2441,9 @@ + + + @@ -2473,6 +2481,9 @@ + + + @@ -2510,6 +2521,9 @@ + + + @@ -2597,10 +2611,8 @@ - - - + @@ -2809,6 +2821,9 @@ + + + @@ -2841,9 +2856,6 @@ - - - @@ -2851,9 +2863,6 @@ - - - @@ -2863,31 +2872,6 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/sources/pyside6/PySide6/QtGui/typesystem_gui_nativeinterface.xml b/sources/pyside6/PySide6/QtGui/typesystem_gui_nativeinterface.xml new file mode 100644 index 00000000..ba83d0f1 --- /dev/null +++ b/sources/pyside6/PySide6/QtGui/typesystem_gui_nativeinterface.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sources/pyside6/PySide6/QtGui/typesystem_gui_rhi.xml b/sources/pyside6/PySide6/QtGui/typesystem_gui_rhi.xml index 656f18ca..c594a043 100644 --- a/sources/pyside6/PySide6/QtGui/typesystem_gui_rhi.xml +++ b/sources/pyside6/PySide6/QtGui/typesystem_gui_rhi.xml @@ -4,6 +4,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only --> + @@ -133,7 +134,14 @@ - + + + + diff --git a/sources/pyside6/PySide6/QtGui/typesystem_gui_wayland.xml b/sources/pyside6/PySide6/QtGui/typesystem_gui_wayland.xml new file mode 100644 index 00000000..5b822d4f --- /dev/null +++ b/sources/pyside6/PySide6/QtGui/typesystem_gui_wayland.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/sources/pyside6/PySide6/QtMultimedia/CMakeLists.txt b/sources/pyside6/PySide6/QtMultimedia/CMakeLists.txt index 8e6d3e3c..8d571642 100644 --- a/sources/pyside6/PySide6/QtMultimedia/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtMultimedia/CMakeLists.txt @@ -31,8 +31,10 @@ ${QtMultimedia_GEN_DIR}/qmediaplayer_wrapper.cpp ${QtMultimedia_GEN_DIR}/qmediarecorder_wrapper.cpp ${QtMultimedia_GEN_DIR}/qmediatimerange_wrapper.cpp ${QtMultimedia_GEN_DIR}/qmediatimerange_interval_wrapper.cpp +${QtMultimedia_GEN_DIR}/qplaybackoptions_wrapper.cpp ${QtMultimedia_GEN_DIR}/qscreencapture_wrapper.cpp ${QtMultimedia_GEN_DIR}/qsoundeffect_wrapper.cpp +${QtMultimedia_GEN_DIR}/qtaudio_wrapper.cpp ${QtMultimedia_GEN_DIR}/qtvideo_wrapper.cpp ${QtMultimedia_GEN_DIR}/qvideoframe_paintoptions_wrapper.cpp ${QtMultimedia_GEN_DIR}/qvideoframe_wrapper.cpp diff --git a/sources/pyside6/PySide6/QtMultimedia/typesystem_multimedia.xml b/sources/pyside6/PySide6/QtMultimedia/typesystem_multimedia.xml index 18082888..cbb39276 100644 --- a/sources/pyside6/PySide6/QtMultimedia/typesystem_multimedia.xml +++ b/sources/pyside6/PySide6/QtMultimedia/typesystem_multimedia.xml @@ -13,9 +13,30 @@ - + + + + + + + + + + + + + + + + + + + + + + + @@ -125,6 +146,7 @@ + @@ -143,6 +165,10 @@ + + + + diff --git a/sources/pyside6/PySide6/QtQml/pysideqmlvolatilebool.cpp b/sources/pyside6/PySide6/QtQml/pysideqmlvolatilebool.cpp index 2cab76b4..d5c2e920 100644 --- a/sources/pyside6/PySide6/QtQml/pysideqmlvolatilebool.cpp +++ b/sources/pyside6/PySide6/QtQml/pysideqmlvolatilebool.cpp @@ -88,14 +88,10 @@ static PyMethodDef QtQml_VolatileBoolObject_methods[] = { static PyObject * QtQml_VolatileBoolObject_repr(QtQml_VolatileBoolObject *self) { - PyObject *s; - - if (*self->flag) - s = PyBytes_FromFormat("%s(True)", - Py_TYPE(self)->tp_name); - else - s = PyBytes_FromFormat("%s(False)", - Py_TYPE(self)->tp_name); + const char *typeName = Py_TYPE(reinterpret_cast(self))->tp_name; + PyObject *s = *self->flag + ? PyBytes_FromFormat("%s(True)", typeName) + : PyBytes_FromFormat("%s(False)", typeName); Py_XINCREF(s); return s; } @@ -103,14 +99,10 @@ QtQml_VolatileBoolObject_repr(QtQml_VolatileBoolObject *self) static PyObject * QtQml_VolatileBoolObject_str(QtQml_VolatileBoolObject *self) { - PyObject *s; - - if (*self->flag) - s = PyBytes_FromFormat("%s(True) -> %p", - Py_TYPE(self)->tp_name, self->flag); - else - s = PyBytes_FromFormat("%s(False) -> %p", - Py_TYPE(self)->tp_name, self->flag); + const char *typeName = Py_TYPE(reinterpret_cast(self))->tp_name; + PyObject *s = *self->flag + ? PyBytes_FromFormat("%s(True) -> %p", typeName, self->flag) + : PyBytes_FromFormat("%s(False) -> %p", typeName, self->flag); Py_XINCREF(s); return s; } @@ -150,13 +142,14 @@ static const char *VolatileBool_SignatureStrings[] = { void initQtQmlVolatileBool(PyObject *module) { - if (InitSignatureStrings(QtQml_VolatileBool_TypeF(), VolatileBool_SignatureStrings) < 0) { + auto *qmlVolatileBoolType = QtQml_VolatileBool_TypeF(); + if (InitSignatureStrings(qmlVolatileBoolType, VolatileBool_SignatureStrings) < 0) { PyErr_Print(); qWarning() << "Error initializing VolatileBool type."; return; } - Py_INCREF(QtQml_VolatileBool_TypeF()); - PyModule_AddObject(module, PepType_GetNameStr(QtQml_VolatileBool_TypeF()), - reinterpret_cast(QtQml_VolatileBool_TypeF())); + auto *obQmlVolatileBoolType = reinterpret_cast(qmlVolatileBoolType); + Py_INCREF(obQmlVolatileBoolType); + PepModule_AddType(module, qmlVolatileBoolType); } diff --git a/sources/pyside6/PySide6/QtQuick/pysidequickregistertype.cpp b/sources/pyside6/PySide6/QtQuick/pysidequickregistertype.cpp index f7749b4e..e92c06a4 100644 --- a/sources/pyside6/PySide6/QtQuick/pysidequickregistertype.cpp +++ b/sources/pyside6/PySide6/QtQuick/pysidequickregistertype.cpp @@ -6,9 +6,12 @@ #include #include #include -#include +#include +#include +#include #include +#include #if QT_CONFIG(opengl) || QT_CONFIG(opengles2) || QT_CONFIG(opengles3) # include @@ -71,6 +74,7 @@ void PySide::initQuickSupport(PyObject *module) qRegisterMetaType("QQuickFramebufferObject*"); #endif qRegisterMetaType("QQuickItem*"); + qRegisterMetaType("QQuickTextDocument*"); Qml::setQuickRegisterItemFunction(quickRegisterType); } diff --git a/sources/pyside6/PySide6/QtSql/typesystem_sql.xml b/sources/pyside6/PySide6/QtSql/typesystem_sql.xml index 70c3e6f6..451c191d 100644 --- a/sources/pyside6/PySide6/QtSql/typesystem_sql.xml +++ b/sources/pyside6/PySide6/QtSql/typesystem_sql.xml @@ -20,6 +20,7 @@ + @@ -29,6 +30,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/sources/pyside6/PySide6/QtUiTools/typesystem_uitools.xml b/sources/pyside6/PySide6/QtUiTools/typesystem_uitools.xml index 9cfa176c..c8e98d96 100644 --- a/sources/pyside6/PySide6/QtUiTools/typesystem_uitools.xml +++ b/sources/pyside6/PySide6/QtUiTools/typesystem_uitools.xml @@ -13,7 +13,8 @@ - + diff --git a/sources/pyside6/PySide6/QtWebEngineCore/CMakeLists.txt b/sources/pyside6/PySide6/QtWebEngineCore/CMakeLists.txt index 7afb9ef1..4d934438 100644 --- a/sources/pyside6/PySide6/QtWebEngineCore/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtWebEngineCore/CMakeLists.txt @@ -20,6 +20,8 @@ ${QtWebEngineCore_GEN_DIR}/qwebenginecookiestore_wrapper.cpp ${QtWebEngineCore_GEN_DIR}/qwebenginecookiestore_filterrequest_wrapper.cpp ${QtWebEngineCore_GEN_DIR}/qwebenginedesktopmediarequest_wrapper.cpp ${QtWebEngineCore_GEN_DIR}/qwebenginedownloadrequest_wrapper.cpp +${QtWebEngineCore_GEN_DIR}/qwebengineextensioninfo_wrapper.cpp +${QtWebEngineCore_GEN_DIR}/qwebengineextensionmanager_wrapper.cpp ${QtWebEngineCore_GEN_DIR}/qwebenginefilesystemaccessrequest_wrapper.cpp ${QtWebEngineCore_GEN_DIR}/qwebenginefindtextresult_wrapper.cpp ${QtWebEngineCore_GEN_DIR}/qwebengineframe_wrapper.cpp diff --git a/sources/pyside6/PySide6/QtWebEngineCore/typesystem_webenginecore.xml b/sources/pyside6/PySide6/QtWebEngineCore/typesystem_webenginecore.xml index 003c18de..3c15f4ee 100644 --- a/sources/pyside6/PySide6/QtWebEngineCore/typesystem_webenginecore.xml +++ b/sources/pyside6/PySide6/QtWebEngineCore/typesystem_webenginecore.xml @@ -52,6 +52,9 @@ + + + diff --git a/sources/pyside6/PySide6/QtWebView/CMakeLists.txt b/sources/pyside6/PySide6/QtWebView/CMakeLists.txt index 158c720c..a8ac8c2a 100644 --- a/sources/pyside6/PySide6/QtWebView/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtWebView/CMakeLists.txt @@ -25,7 +25,8 @@ set(QtWebView_libraries pyside6 set(QtWebView_deps QtGui) # for Windows and Linux, QtWebView depends on QtWebEngine to render content -if ((WIN32 OR UNIX) AND NOT APPLE) +# On Android, QtWebView uses the native webview backend and does not require QtWebEngine. +if ((WIN32 OR UNIX) AND NOT APPLE AND NOT ANDROID) list(APPEND QtWebView_deps QtWebEngineCore QtWebEngineQuick) endif() diff --git a/sources/pyside6/PySide6/QtWidgets/typesystem_widgets_common.xml b/sources/pyside6/PySide6/QtWidgets/typesystem_widgets_common.xml index f1b9e14d..8058c752 100644 --- a/sources/pyside6/PySide6/QtWidgets/typesystem_widgets_common.xml +++ b/sources/pyside6/PySide6/QtWidgets/typesystem_widgets_common.xml @@ -233,7 +233,7 @@ - + @@ -245,7 +245,7 @@ - + diff --git a/sources/pyside6/PySide6/QtXml/typesystem_xml.xml b/sources/pyside6/PySide6/QtXml/typesystem_xml.xml index 089978b6..93d3c1f5 100644 --- a/sources/pyside6/PySide6/QtXml/typesystem_xml.xml +++ b/sources/pyside6/PySide6/QtXml/typesystem_xml.xml @@ -20,7 +20,7 @@ - + @@ -130,6 +130,8 @@ + diff --git a/sources/pyside6/PySide6/__init__.py.in b/sources/pyside6/PySide6/__init__.py.in index 197eba96..c1050f2c 100644 --- a/sources/pyside6/PySide6/__init__.py.in +++ b/sources/pyside6/PySide6/__init__.py.in @@ -64,9 +64,9 @@ def _setupQtDirectories(): # setting dictates. There is no longer a difference in path structure. global Shiboken from shiboken6 import Shiboken - except Exception: + except Exception as e: paths = ', '.join(sys.path) - print(f"PySide6/__init__.py: Unable to import Shiboken from {paths}", + print(f"PySide6/__init__.py: Unable to import Shiboken from {paths}: {e}", file=sys.stderr) raise diff --git a/sources/pyside6/PySide6/doc/qtcore.rst b/sources/pyside6/PySide6/doc/qtcore.rst index b8d551e7..eb369ee7 100644 --- a/sources/pyside6/PySide6/doc/qtcore.rst +++ b/sources/pyside6/PySide6/doc/qtcore.rst @@ -116,3 +116,12 @@ Example:: logging.debug("Test debug message") // @snippet qmessagelogger + +// @snippet qrangemodel-numpy-constructor +The function takes one-dimensional or two-dimensional numpy arrays of various +integer or float types to populate an editable QRangeModel. +// @snippet qrangemodel-numpy-constructor + +// @snippet qrangemodel-sequence-constructor +The function takes a sequence of of data to populate a read-only QRangeModel. +// @snippet qrangemodel-sequence-constructor diff --git a/sources/pyside6/PySide6/global.h.in b/sources/pyside6/PySide6/global.h.in deleted file mode 100644 index 9a1e001f..00000000 --- a/sources/pyside6/PySide6/global.h.in +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (C) 2020 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#include - -#if @ENABLE_MAC@ -# define Q_OS_MAC -#endif -#if @ENABLE_WIN@ -# define Q_OS_WIN -#endif -#if @ENABLE_UNIX@ -# define Q_OS_UNIX -#endif - -// There are symbols in Qt that exist in Debug but -// not in release -#define QT_NO_DEBUG - -// Here are now all configured modules appended: diff --git a/sources/pyside6/PySide6/glue/qtcore.cpp b/sources/pyside6/PySide6/glue/qtcore.cpp index 0a5019bf..8e206109 100644 --- a/sources/pyside6/PySide6/glue/qtcore.cpp +++ b/sources/pyside6/PySide6/glue/qtcore.cpp @@ -33,7 +33,7 @@ QArgData qArgDataFromPyType(PyObject *t) const char *typeName{}; if (PyType_Check(t)) { auto *pyType = reinterpret_cast(t); - typeName = pyType->tp_name; + typeName = PepType_GetFullyQualifiedNameStr(pyType); result.metaType = PySide::qMetaTypeFromPyType(pyType); } else if (PyUnicode_Check(t)) { typeName = Shiboken::String::toCString(t); @@ -225,49 +225,6 @@ return %out; // @snippet conversion-qmetatype-pytypeobject // @snippet qvariant-conversion -static QVariant QVariant_convertToVariantMap(PyObject *map) -{ - Py_ssize_t pos = 0; - Shiboken::AutoDecRef keys(PyDict_Keys(map)); - if (!QVariant_isStringList(keys)) - return {}; - PyObject *key{}; - PyObject *value{}; - QMap ret; - while (PyDict_Next(map, &pos, &key, &value)) { - QString cppKey = %CONVERTTOCPP[QString](key); - QVariant cppValue = %CONVERTTOCPP[QVariant](value); - ret.insert(cppKey, cppValue); - } - return QVariant(ret); -} -static QVariant QVariant_convertToVariantList(PyObject *list) -{ - if (QVariant_isStringList(list)) { - QList lst = %CONVERTTOCPP[QList](list); - return QVariant(QStringList(lst)); - } - QVariant valueList = QVariant_convertToValueList(list); - if (valueList.isValid()) - return valueList; - - if (PySequence_Size(list) < 0) { - // clear the error if < 0 which means no length at all - PyErr_Clear(); - return {}; - } - - QList lst; - Shiboken::AutoDecRef fast(PySequence_Fast(list, "Failed to convert QVariantList")); - const Py_ssize_t size = PySequence_Size(fast.object()); - for (Py_ssize_t i = 0; i < size; ++i) { - Shiboken::AutoDecRef pyItem(PySequence_GetItem(fast.object(), i)); - QVariant item = %CONVERTTOCPP[QVariant](pyItem); - lst.append(item); - } - return QVariant(lst); -} - using SpecificConverter = Shiboken::Conversions::SpecificConverter; static std::optional converterForQtType(const char *typeNameC) @@ -323,7 +280,7 @@ QList version = QByteArray(qVersion()).split('.'); PyObject *pyQtVersion = PyTuple_New(3); for (int i = 0; i < 3; ++i) PyTuple_SetItem(pyQtVersion, i, PyLong_FromLong(version[i].toInt())); -PyModule_AddObject(module, "__version_info__", pyQtVersion); +PepModule_Add(module, "__version_info__", pyQtVersion); PyModule_AddStringConstant(module, "__version__", qVersion()); // @snippet qt-version @@ -433,10 +390,7 @@ static PyObject *qtmsghandler = nullptr; static void msgHandlerCallback(QtMsgType type, const QMessageLogContext &ctx, const QString &msg) { Shiboken::GilState state; - PyObject *excType{}; - PyObject *excValue{}; - PyObject *excTraceback{}; - PyErr_Fetch(&excType, &excValue, &excTraceback); + Shiboken::Errors::Stash errorStash; Shiboken::AutoDecRef arglist(PyTuple_New(3)); PyTuple_SetItem(arglist, 0, %CONVERTTOPYTHON[QtMsgType](type)); PyTuple_SetItem(arglist, 1, %CONVERTTOPYTHON[QMessageLogContext &](ctx)); @@ -444,7 +398,6 @@ static void msgHandlerCallback(QtMsgType type, const QMessageLogContext &ctx, co const char *data = array.constData(); PyTuple_SetItem(arglist, 2, %CONVERTTOPYTHON[const char *](data)); Shiboken::AutoDecRef ret(PyObject_CallObject(qtmsghandler, arglist)); - PyErr_Restore(excType, excValue, excTraceback); } // @snippet qt-messagehandler @@ -539,6 +492,12 @@ QTime time(%4, %5, %6, %7); Shiboken::Warnings::warnDeprecated("QDateTime", "QDateTime(..., Qt::TimeSpec spec)"); // @snippet qdatetime-3 +// @snippet qdatetime-4 +QDate date(%1, %2, %3); +QTime time(%4, %5, %6, %7); +%0 = new %TYPE(date, time, QTimeZone(%8)); +// @snippet qdatetime-4 + // @snippet qdatetime-topython QDate date = %CPPSELF.date(); QTime time = %CPPSELF.time(); @@ -670,9 +629,12 @@ if (PySlice_Check(_key) == 0) "list indices must be integers or slices, not %.200s", Py_TYPE(_key)->tp_name); -Py_ssize_t start, stop, step, slicelength; -if (PySlice_GetIndicesEx(_key, %CPPSELF.size(), &start, &stop, &step, &slicelength) < 0) +Py_ssize_t start{}; +Py_ssize_t stop{}; +Py_ssize_t step{}; +if (PySlice_Unpack(_key, &start, &stop, &step) < 0) return nullptr; +Py_ssize_t slicelength = PySlice_AdjustIndices(%CPPSELF.size(), &start, &stop, step); QByteArray ba; if (slicelength <= 0) @@ -745,9 +707,12 @@ if (PySlice_Check(_key) == 0) { return -1; } -Py_ssize_t start, stop, step, slicelength; -if (PySlice_GetIndicesEx(_key, %CPPSELF.size(), &start, &stop, &step, &slicelength) < 0) +Py_ssize_t start{}; +Py_ssize_t stop{}; +Py_ssize_t step{}; +if (PySlice_Unpack(_key, &start, &stop, &step) < 0) return -1; +const Py_ssize_t slicelength = PySlice_AdjustIndices(%CPPSELF.size(), &start, &stop, step); // The parameter candidates are: bytes/str, bytearray, QByteArray itself. // Not supported are iterables containing ints between 0~255 @@ -1535,7 +1500,15 @@ double in = %CONVERTTOCPP[double](%in); // @snippet conversion-sbkobject // a class supported by QVariant? -const QMetaType metaType = QVariant_resolveMetaType(Py_TYPE(%in)); +QMetaType metaType; +if (Shiboken::Enum::check(%in)) { + const auto typeName = PySide::QEnum::getTypeName(Py_TYPE(%in)); + if (!typeName.isEmpty()) + metaType = QMetaType::fromName(typeName); +} +if (!metaType.isValid()) + metaType = PySide::Variant::resolveMetaType(Py_TYPE(%in)); + bool ok = false; if (metaType.isValid()) { QVariant var(metaType); @@ -1556,12 +1529,12 @@ if (!ok) // @snippet conversion-sbkobject // @snippet conversion-pydict -QVariant ret = QVariant_convertToVariantMap(%in); +QVariant ret = PySide::Variant::convertToVariantMap(%in); %out = ret.isValid() ? ret : QVariant::fromValue(PySide::PyObjectWrapper(%in)); // @snippet conversion-pydict // @snippet conversion-pylist -QVariant ret = QVariant_convertToVariantList(%in); +QVariant ret = PySide::Variant::convertToVariantList(%in); %out = ret.isValid() ? ret : QVariant::fromValue(PySide::PyObjectWrapper(%in)); // @snippet conversion-pylist @@ -1571,7 +1544,7 @@ QVariant ret = QVariant_convertToVariantList(%in); // @snippet conversion-pyobject // @snippet conversion-qjsonobject-pydict -QVariant dict = QVariant_convertToVariantMap(%in); +QVariant dict = PySide::Variant::convertToVariantMap(%in); QJsonValue val = QJsonValue::fromVariant(dict); %out = val.toObject(); // @snippet conversion-qjsonobject-pydict @@ -1853,6 +1826,43 @@ if (Shiboken::Enum::check(%PYARG_2)) cppArg1 = QVariant(int(Shiboken::Enum::getValue(%PYARG_2))); // @snippet qmetaproperty_write_enum +// @snippet qmetaenum_value +auto valueOpt = %CPPSELF.value64(%1); +if (valueOpt.has_value()) { + const quint64 ullValue = valueOpt.value(); + %PYARG_0 = PyLong_FromUnsignedLongLong(ullValue); +} else { + const int lValue = %CPPSELF.%FUNCTION_NAME(%1); + %PYARG_0 = PyLong_FromLong(lValue); +} +// @snippet qmetaenum_value + +// @snippet qmetaenum_keytovalue +PyObject *pyLongValue{}; +auto valueOpt = %CPPSELF.keyToValue64(%1); +bool ok_ = valueOpt.has_value(); +if (ok_) + pyLongValue = PyLong_FromUnsignedLongLong(valueOpt.value()); +else + pyLongValue = PyLong_FromLong(%CPPSELF.%FUNCTION_NAME(%1, &ok_)); +%PYARG_0 = PyTuple_New(2); +PyTuple_SetItem(%PYARG_0, 0, pyLongValue); +PyTuple_SetItem(%PYARG_0, 1, %CONVERTTOPYTHON[bool](ok_)); +// @snippet qmetaenum_keytovalue + +// @snippet qmetaenum_keystovalue +PyObject *pyLongValue{}; +auto valueOpt = %CPPSELF.keysToValue64(%1); +bool ok_ = valueOpt.has_value(); +if (ok_) + pyLongValue = PyLong_FromUnsignedLongLong(valueOpt.value()); +else + pyLongValue = PyLong_FromLong(%CPPSELF.%FUNCTION_NAME(%1, &ok_)); +%PYARG_0 = PyTuple_New(2); +PyTuple_SetItem(%PYARG_0, 0, pyLongValue); +PyTuple_SetItem(%PYARG_0, 1, %CONVERTTOPYTHON[bool](ok_)); +// @snippet qmetaenum_keystovalue + // @snippet qdatastream-read-bytes QByteArray data; data.resize(%2); @@ -2172,7 +2182,9 @@ Q_IMPORT_PLUGIN(QDarwinCalendarPermissionPlugin) // @snippet qt-modifier PyObject *_inputDict = PyDict_New(); // Note: The builtins line is no longer needed since Python 3.10. Undocumented! -PyDict_SetItemString(_inputDict, "__builtins__", PyEval_GetBuiltins()); +Shiboken::AutoDecRef builtins(PepEval_GetFrameBuiltins()); +PyDict_SetItemString(_inputDict, "__builtins__", builtins.object()); +builtins.reset(nullptr); PyDict_SetItemString(_inputDict, "QtCore", module); PyDict_SetItemString(_inputDict, "Qt", reinterpret_cast(pyType)); // Explicitly not dereferencing the result. @@ -2219,7 +2231,7 @@ if (%CPPSELF.next()) { // @snippet qdirlistingiterator-next // @snippet qdirlisting-direntry-repr -QByteArray result = '<' + QByteArray(Py_TYPE(%PYSELF)->tp_name) +QByteArray result = '<' + QByteArray(PepType_GetFullyQualifiedNameStr(Py_TYPE(%PYSELF))) + " object at 0x" + QByteArray::number(quintptr(%PYSELF), 16) + " (\"" + %CPPSELF.absoluteFilePath().toUtf8() + "\")>"; @@ -2243,3 +2255,252 @@ if (PySequence_Check(%PYARG_0) != 0 && PySequence_Size(%PYARG_0) == 2) { PyTuple_SetItem(%PYARG_0, 0, %CONVERTTOPYTHON[%RETURN_TYPE](%0)); PyTuple_SetItem(%PYARG_0, 1, %CONVERTTOPYTHON[qintptr](*result_out)); // @snippet return-native-eventfilter + + +// @snippet qrangemodel-wrapper +// Import the template constructors +using QRangeModel::QRangeModel; +// @snippet qrangemodel-wrapper + +// @snippet qrangemodel-helper-functions +template +static inline QSpan createSpan(void *vData, Py_ssize_t size) +{ + auto *data = reinterpret_cast(vData); + return QSpan{data, data + size}; +} + +// Simple 2d table range for creating a QRangeModel +// (potentially replaceable by a std::mdspan in C++ 23). +template +class TableRange +{ + struct TableData + { + T *data = nullptr; + qsizetype rowCount = -1; + qsizetype columCount = -1; + }; + +public: + explicit TableRange(void *data, qsizetype rowCount, qsizetype columCount) : + m_data{reinterpret_cast(data), rowCount, columCount} {} + + class Iterator + { + public: + using value_type = QSpan; + using size_type = qsizetype; + using reference = value_type; + using pointer = value_type; + using difference_type = std::ptrdiff_t; + using iterator_category = std::random_access_iterator_tag; + + explicit Iterator(const TableData &data, size_type row) noexcept: + m_data(data), m_row(row) {} + + Iterator() = default; + + constexpr Iterator &operator++() noexcept + { + Q_ASSERT(m_row < m_data.rowCount); + ++m_row; + return *this; + } + + constexpr Iterator operator++(int) noexcept + { + Q_ASSERT(m_row < m_data.rowCount); + auto copy = *this; + ++m_row; + return copy; + } + + constexpr Iterator &operator--() noexcept + { + Q_ASSERT(m_row > 0); + --m_row; + return *this; + } + + constexpr Iterator operator--(int) noexcept + { + Q_ASSERT(m_row > 0); + auto copy = *this; + --m_row; + return copy; + } + + Iterator &operator+=(difference_type i) + { + const auto row = m_row + i; + Q_ASSERT(row >= 0 && row <= m_data.rowCount); + m_row = row; + return *this; + } + + Iterator &operator-=(difference_type i) + { + const auto row = m_row - i; + Q_ASSERT(row >= 0 && row <= m_data.rowCount); + m_row = row; + return *this; + } + + Iterator operator+(difference_type i) const + { + const auto row = m_row + i; + Q_ASSERT(row >= 0 && row <= m_data.rowCount); + return {m_data, row}; + } + + Iterator operator-(difference_type i) const + { + const auto row = m_row - i; + Q_ASSERT(row >= 0 && row <= m_data.rowCount); + return {m_data, row}; + } + + difference_type operator-(const Iterator &it) const { return m_row - it.m_row; } // std::distance + + reference operator*() const noexcept + { + auto *rowStart = m_data.data + m_row * m_data.columCount; + return {rowStart, rowStart + m_data.columCount}; + } + + [[nodiscard]] value_type operator[](difference_type i) const + { + auto *rowStart = m_data.data + (m_row + i) * m_data.columCount; + return {rowStart, rowStart + m_data.columCount}; + } + + private: + friend bool comparesEqual(const Iterator &lhs, const Iterator &rhs) noexcept + { + Q_ASSERT(lhs.m_data.data != nullptr); + Q_ASSERT(lhs.m_data.data == rhs.m_data.data); + return lhs.m_row == rhs.m_row; + } + + friend Qt::strong_ordering compareThreeWay(const Iterator &lhs, + const Iterator &rhs) noexcept + { + Q_ASSERT(lhs.m_data.data != nullptr); + Q_ASSERT(lhs.m_data.data == rhs.m_data.data); + return Qt::compareThreeWay(lhs.m_row, rhs.m_row); + } + + Q_DECLARE_STRONGLY_ORDERED(Iterator) + + TableData m_data; + size_type m_row = 0; + }; + + [[nodiscard]] Iterator begin() const { return Iterator(m_data, 0); } + [[nodiscard]] Iterator end() const { return Iterator(m_data, m_data.rowCount); } + +private: + TableData m_data; +}; + +template // QRangeModelWrapper +static RangeModel *createRangeModel(PyObject *in, QObject *parent) +{ + auto view = Shiboken::Numpy::View::fromPyObject(in); + if (!view) { + PyErr_SetString(PyExc_TypeError, "Invalid parameter or missing numpy support."); + return nullptr; + } + switch (view.ndim) { + case 1: { + const auto size = view.dimensions[0]; + switch (view.type) { + case Shiboken::Numpy::View::Int16: + return new RangeModel(createSpan(view.data, size), parent); + case Shiboken::Numpy::View::Unsigned16: + return new RangeModel(createSpan(view.data, size), parent); + case Shiboken::Numpy::View::Int: + return new RangeModel(createSpan(view.data, size), parent); + case Shiboken::Numpy::View::Unsigned: + return new RangeModel(createSpan(view.data, size), parent); + case Shiboken::Numpy::View::Int64: + return new RangeModel(createSpan(view.data, size), parent); + case Shiboken::Numpy::View::Unsigned64: + return new RangeModel(createSpan(view.data, size), parent); + case Shiboken::Numpy::View::Float: + return new RangeModel(createSpan(view.data, size), parent); + case Shiboken::Numpy::View::Double: + return new RangeModel(createSpan(view.data, size), parent); + default: + PyErr_SetString(PyExc_TypeError, "Unsupported data type for one-dimensional arrays."); + return nullptr; + } + } + break; + + case 2: { + const auto rows = view.dimensions[0]; + const auto columns = view.dimensions[1]; + switch (view.type) { + case Shiboken::Numpy::View::Int16: + return new RangeModel(TableRange(view.data, rows, columns), parent); + case Shiboken::Numpy::View::Unsigned16: + return new RangeModel(TableRange(view.data, rows, columns), parent); + case Shiboken::Numpy::View::Int: + return new RangeModel(TableRange(view.data, rows, columns), parent); + case Shiboken::Numpy::View::Unsigned: + return new RangeModel(TableRange(view.data, rows, columns), parent); + case Shiboken::Numpy::View::Int64: + return new RangeModel(TableRange(view.data, rows, columns), parent); + case Shiboken::Numpy::View::Unsigned64: + return new RangeModel(TableRange(view.data, rows, columns), parent); + case Shiboken::Numpy::View::Float: + return new RangeModel(TableRange(view.data, rows, columns), parent); + case Shiboken::Numpy::View::Double: + return new RangeModel(TableRange(view.data, rows, columns), parent); + default: + PyErr_SetString(PyExc_TypeError, "Unsupported data type for two-dimensional arrays."); + return nullptr; + } + } + break; + default: + PyErr_SetString(PyExc_TypeError, "Only one and two-dimensional arrays are supported."); + return nullptr; + } + return nullptr; +} + +static bool isVariantList(const QVariant &v) +{ + return v.typeId() == QMetaType::QVariantList; +}; +// @snippet qrangemodel-helper-functions + +// @snippet qrangemodel-numpy-constructor +auto *model = createRangeModel<%TYPE>(%PYARG_1, %2); +if (model == nullptr) + return -1; +%0 = model; +// @snippet qrangemodel-numpy-constructor + +// @snippet qrangemodel-sequence-constructor +const auto vlOptional = PySide::Variant::pyListToVariantList(%PYARG_1); +if (!vlOptional.has_value()) { + PyErr_SetString(PyExc_TypeError, "Unable convert input sequence."); + return -1; +} + +const QVariantList &vList = vlOptional.value(); +if (!vList.isEmpty() && std::all_of(vList.cbegin(), vList.cend(), isVariantList)) { + // Empirical: Transform QVariantList -> QList for a table + QList variantTable; + variantTable.reserve(vList.size()); + for (const auto &rowV : vList) + variantTable.append(rowV.value()); + %0 = new %TYPE(variantTable, %2); +} else { + %0 = new %TYPE(vList, %2); +} +// @snippet qrangemodel-sequence-constructor diff --git a/sources/pyside6/PySide6/glue/qtgui.cpp b/sources/pyside6/PySide6/glue/qtgui.cpp index 72d3d2b4..4b8e718a 100644 --- a/sources/pyside6/PySide6/glue/qtgui.cpp +++ b/sources/pyside6/PySide6/glue/qtgui.cpp @@ -365,6 +365,11 @@ for (Py_ssize_t i = 0; i < count; ++i){ %PYARG_0 = %CONVERTTOPYTHON[QPolygon *](%CPPSELF); // @snippet qpolygon-operatorlowerlower +// @snippet qpolygonf-operatorlowerlower +*%CPPSELF << %1; +%PYARG_0 = %CONVERTTOPYTHON[QPolygonF *](%CPPSELF); +// @snippet qpolygonf-operatorlowerlower + // @snippet qpixmap %0 = new %TYPE(QPixmap::fromImage(%1)); // @snippet qpixmap @@ -853,7 +858,7 @@ for (Py_ssize_t i = 0; i < 16; ++i) { } // @snippet qmatrix4x4-copydatato -// @snippet qmatrix4x4-mgetitem +// @snippet qmatrix-mgetitem if (PySequence_Check(_key)) { Shiboken::AutoDecRef key(PySequence_Fast(_key, "Invalid matrix index.")); if (PySequence_Size(key.object()) == 2) { @@ -867,7 +872,7 @@ if (PySequence_Check(_key)) { } PyErr_SetString(PyExc_IndexError, "Invalid matrix index."); return 0; -// @snippet qmatrix4x4-mgetitem +// @snippet qmatrix-mgetitem // @snippet qguiapplication-init static void QGuiApplicationConstructor(PyObject *self, PyObject *pyargv, QGuiApplicationWrapper **cptr) @@ -907,7 +912,13 @@ if (auto *x11App = %CPPSELF.nativeInterface() hasNativeApp = true; %PYARG_0 = %CONVERTTOPYTHON[QNativeInterface::QX11Application*](x11App); } -#endif +#endif // xcb +#if QT_CONFIG(wayland) +if (auto *waylandApp = %CPPSELF.nativeInterface()) { + hasNativeApp = true; + %PYARG_0 = %CONVERTTOPYTHON[QNativeInterface::QWaylandApplication*](waylandApp); +} +#endif // wayland if (!hasNativeApp) { Py_INCREF(Py_None); %PYARG_0 = Py_None; @@ -934,10 +945,11 @@ if (!hasNativeScreen) { } // @snippet qscreen-nativeInterface -// @snippet qx11application-resource-ptr +// Return 'int' from native interface's forward-declared structs like Display* +// @snippet native-resource-ptr auto *resource = %CPPSELF.%FUNCTION_NAME(); %PYARG_0 = PyLong_FromVoidPtr(resource); -// @snippet qx11application-resource-ptr +// @snippet native-resource-ptr // @snippet qwindow-fromWinId WId id = %1; @@ -1055,7 +1067,7 @@ return %CONVERTTOPYTHON[QRect](cppResult); // @snippet qpainterstateguard-restore // @snippet qmatrix-repr-code -QByteArray format(Py_TYPE(%PYSELF)->tp_name); +QByteArray format(PepType_GetFullyQualifiedNameStr(Py_TYPE(%PYSELF))); format += QByteArrayLiteral("(("); %MATRIX_TYPE data[%MATRIX_SIZE]; %CPPSELF.copyDataTo(data); diff --git a/sources/pyside6/PySide6/glue/qtmultimedia.cpp b/sources/pyside6/PySide6/glue/qtmultimedia.cpp index ac8434b9..45762163 100644 --- a/sources/pyside6/PySide6/glue/qtmultimedia.cpp +++ b/sources/pyside6/PySide6/glue/qtmultimedia.cpp @@ -22,7 +22,7 @@ const auto size = %CPPSELF.byteCount(); %PYARG_0 = Shiboken::Buffer::newObject(data, size); // @snippet qaudiobuffer-const-data -// @snippet qtaudio-namespace-compatibility-alias -Py_INCREF(pyType); -PyModule_AddObject(module, "QtAudio", reinterpret_cast(pyType)); -// @snippet qtaudio-namespace-compatibility-alias +// @snippet qaudio-convertvolume +const float result = QtAudio::convertVolume(%1, %2, %3); +%PYARG_0 = %CONVERTTOPYTHON[float](result); +// @snippet qaudio-convertvolume diff --git a/sources/pyside6/PySide6/glue/qttest.cpp b/sources/pyside6/PySide6/glue/qttest.cpp index 6d6336a8..b7140828 100644 --- a/sources/pyside6/PySide6/glue/qttest.cpp +++ b/sources/pyside6/PySide6/glue/qttest.cpp @@ -15,7 +15,7 @@ if (!signature.isEmpty()) if (emitter == nullptr || signature.isEmpty()) { QByteArray error = QByteArrayLiteral("Wrong parameter (") - + (%PYARG_1)->ob_type->tp_name + + PepType_GetFullyQualifiedNameStr(Py_TYPE(%PYARG_1)) + QByteArrayLiteral(") passed, QSignalSpy requires a signal."); PyErr_SetString(PyExc_ValueError, error.constData()); return -1; diff --git a/sources/pyside6/PySide6/glue/qtuitools.cpp b/sources/pyside6/PySide6/glue/qtuitools.cpp index 3fe3554c..6eba9a94 100644 --- a/sources/pyside6/PySide6/glue/qtuitools.cpp +++ b/sources/pyside6/PySide6/glue/qtuitools.cpp @@ -7,7 +7,8 @@ * Frédéric */ -#include +#include +#include #include #include diff --git a/sources/pyside6/PySide6/pyside6_global.h b/sources/pyside6/PySide6/pyside6_global.h new file mode 100644 index 00000000..fe417b60 --- /dev/null +++ b/sources/pyside6/PySide6/pyside6_global.h @@ -0,0 +1,10 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include + +// There are symbols in Qt that exist in Debug but +// not in release +#define QT_NO_DEBUG + +// Here are now all configured modules appended: diff --git a/sources/pyside6/PySide6/templates/core_common.xml b/sources/pyside6/PySide6/templates/core_common.xml index 5149f1a7..b3bdd2b9 100644 --- a/sources/pyside6/PySide6/templates/core_common.xml +++ b/sources/pyside6/PySide6/templates/core_common.xml @@ -158,7 +158,7 @@ diff --git a/sources/pyside6/cmake/Macros/PySideModules.cmake b/sources/pyside6/cmake/Macros/PySideModules.cmake index 5cd12b68..86791f4c 100644 --- a/sources/pyside6/cmake/Macros/PySideModules.cmake +++ b/sources/pyside6/cmake/Macros/PySideModules.cmake @@ -210,27 +210,18 @@ macro(create_pyside_module) # comes as a default requirement for building PySide6. As such for # cross-compiling in linux, we use the clang compiler from the installed # libclang itself. - if(CMAKE_ANDROID_ARCH_LLVM_TRIPLE AND CMAKE_HOST_APPLE) - message(STATUS "Building for Android with arch ${CMAKE_ANDROID_ARCH_LLVM_TRIPLE}") - list(APPEND shiboken_command "--clang-option=--target=${CMAKE_ANDROID_ARCH_LLVM_TRIPLE}") - - # CMAKE_CXX_ANDROID_TOOLCHAIN_PREFIX does not contain the ANDROID_PLATFORM i.e. it ends with - # the form 'aarch64-linux-android-'. Remove the last '-' and add the corresponding clang - # based on ANDROID_PLATFORM making it 'aarch64-linux-android26-clang++' - - # Get the length of the string - string(LENGTH "${CMAKE_CXX_ANDROID_TOOLCHAIN_PREFIX}" _length) - - # Subtract 1 from the length to get the characters till '-' - math(EXPR _last_index "${_length} - 1") - - # Get the substring from the start to the character before the last one - string(SUBSTRING "${CMAKE_CXX_ANDROID_TOOLCHAIN_PREFIX}" 0 "${_last_index}" - SHIBOKEN_ANDROID_COMPILER_PREFIX) + if (CMAKE_CROSSCOMPILING) + list(APPEND shiboken_command "--platform=${CMAKE_SYSTEM_NAME}" + "--arch=${CMAKE_SYSTEM_PROCESSOR}" + "--compiler-path=${CMAKE_CXX_COMPILER}") + endif() - # use the compiler from the Android NDK + if(CMAKE_ANDROID_ARCH_LLVM_TRIPLE) + message(STATUS "Building for Android with arch ${CMAKE_ANDROID_ARCH_LLVM_TRIPLE}") + # CMAKE_CXX_COMPILER is the generic clang++; for finding the include paths, + # it needs "--target". list(APPEND shiboken_command - "--compiler-path=${SHIBOKEN_ANDROID_COMPILER_PREFIX}${CMAKE_ANDROID_API}-clang++") + "--compiler-argument=--target=${CMAKE_ANDROID_ARCH_LLVM_TRIPLE}") endif() if(CMAKE_HOST_APPLE) @@ -294,6 +285,24 @@ macro(create_pyside_module) set(ld_prefix_var_name "LD_LIBRARY_PATH") endif() + # Get the build type, default to RELEASE if not set + if(CMAKE_BUILD_TYPE) + string(TOUPPER "${CMAKE_BUILD_TYPE}" _build_type) + else() + set(_build_type "RELEASE") + endif() + + # Try to get the location for the current build type + get_target_property(_shiboken_lib_location Shiboken6::libshiboken IMPORTED_LOCATION_${_build_type}) + + # Fallback to RELEASE if not found + if(NOT _shiboken_lib_location) + get_target_property(_shiboken_lib_location Shiboken6::libshiboken IMPORTED_LOCATION_RELEASE) + endif() + + # Get the directory containing the library file, which is the lib directory + get_filename_component(SHIBOKEN_SHARED_LIBRARY_DIR "${_shiboken_lib_location}" DIRECTORY) + set(ld_prefix_list "") list(APPEND ld_prefix_list "${pysidebindings_BINARY_DIR}/libpyside") list(APPEND ld_prefix_list "${pysidebindings_BINARY_DIR}/libpysideqml") @@ -337,9 +346,11 @@ macro(create_pyside_module) # on the host machine (usually, unless you use some userspace qemu based mechanism). # TODO: Can we do something better here to still get pyi files? if(NOT (PYSIDE_IS_CROSS_BUILD OR DISABLE_PYI)) + set(SHIBOKEN_PYTHON_MODULE_DIR "${PYTHON_SITE_PACKAGES}/shiboken6") set(generate_pyi_options ${module_NAME} --sys-path "${pysidebindings_BINARY_DIR}" - "${SHIBOKEN_PYTHON_MODULE_DIR}/..") # use the layer above shiboken6 + "${SHIBOKEN_PYTHON_MODULE_DIR}/.." + "${SHIBOKEN_PYTHON_MODULE_DIR}/../../..") # use the layer above shiboken6 if (QUIET_BUILD) list(APPEND generate_pyi_options "--quiet") endif() @@ -367,7 +378,7 @@ macro(create_pyside_module) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/PySide6/${module_NAME}/pyside6_${lower_module_name}_python.h - DESTINATION include/PySide6${pyside6_SUFFIX}/${module_NAME}/) + DESTINATION PySide6${pyside6_SUFFIX}/include/${module_NAME}/) file(GLOB typesystem_files ${CMAKE_CURRENT_SOURCE_DIR}/typesystem_*.xml ${typesystem_path}) # Copy typesystem files and remove module names from the element diff --git a/sources/pyside6/cmake/PySideHelpers.cmake b/sources/pyside6/cmake/PySideHelpers.cmake index a4641ee6..9891eab3 100644 --- a/sources/pyside6/cmake/PySideHelpers.cmake +++ b/sources/pyside6/cmake/PySideHelpers.cmake @@ -50,6 +50,7 @@ function(pyside_internal_find_host_shiboken_tools) list(APPEND "REQUIRED") endif() + set(SHIBOKEN6TOOLS_SKIP_FIND_DEPENDENCIES TRUE) find_package( Shiboken6Tools 6 CONFIG ${find_package_extra_args} @@ -132,33 +133,17 @@ macro(collect_optional_modules) list(APPEND ALL_OPTIONAL_MODULES WebChannel WebEngineCore WebEngineWidgets WebEngineQuick WebSockets HttpServer) find_package(Qt${QT_MAJOR_VERSION}WebEngineQuick) - # for Windows and Linux, QtWebView depends on QtWebEngine to render content - if(Qt${QT_MAJOR_VERSION}WebEngineQuick_FOUND OR APPLE) + # For Windows and Linux, QtWebView depends on QtWebEngine to render content. + # On Android and Apple platforms, QtWebView uses the native webview backend and + # does not require QtWebEngine. + if(APPLE OR ANDROID) + list(APPEND ALL_OPTIONAL_MODULES WebView) + elseif(Qt${QT_MAJOR_VERSION}WebEngineQuick_FOUND) list(APPEND ALL_OPTIONAL_MODULES WebView) endif() list(APPEND ALL_OPTIONAL_MODULES 3DCore 3DRender 3DInput 3DLogic 3DAnimation 3DExtras) endmacro() -macro(check_os) - set(ENABLE_UNIX "1") - set(ENABLE_MAC "0") - set(ENABLE_WIN "0") - - # check if Android, if so, set ENABLE_UNIX=1 - # this is needed to avoid including the wrapper specific to macOS when building for Android - # from a macOS host - if(NOT CMAKE_SYSTEM_NAME STREQUAL "Android") - if(CMAKE_HOST_APPLE) - set(ENABLE_MAC "1") - elseif(CMAKE_HOST_WIN32) - set(ENABLE_WIN "1") - set(ENABLE_UNIX "0") - elseif(NOT CMAKE_HOST_UNIX) - message(FATAL_ERROR "OS not supported") - endif() - endif() -endmacro() - macro(use_protected_as_public_hack) # 2017-04-24 The protected hack can unfortunately not be disabled, because # Clang does produce linker errors when we disable the hack. @@ -255,9 +240,10 @@ macro(collect_module_if_found shortname) # record the shortnames for the tests list(APPEND all_module_shortnames ${shortname}) # Build Qt 5 compatibility variables - if(${QT_MAJOR_VERSION} GREATER_EQUAL 6 AND NOT "${shortname}" STREQUAL "OpenGLFunctions") - get_target_property(Qt6${shortname}_INCLUDE_DIRS Qt6::${shortname} - INTERFACE_INCLUDE_DIRECTORIES) + get_target_property(Qt6${shortname}_INCLUDE_DIRS Qt6::${shortname} + INTERFACE_INCLUDE_DIRECTORIES) + # Find QtGui private headers for exposing some QPA classes + if("${shortname}" STREQUAL "Gui") get_target_property(Qt6${shortname}_PRIVATE_INCLUDE_DIRS Qt6::${shortname}Private INTERFACE_INCLUDE_DIRECTORIES) diff --git a/sources/pyside6/cmake/PySideSetup.cmake b/sources/pyside6/cmake/PySideSetup.cmake index 45a63a1a..72d6a1cc 100644 --- a/sources/pyside6/cmake/PySideSetup.cmake +++ b/sources/pyside6/cmake/PySideSetup.cmake @@ -52,6 +52,7 @@ set(BINDING_API_MINOR_VERSION "${pyside_MINOR_VERSION}") set(BINDING_API_MICRO_VERSION "${pyside_MICRO_VERSION}") set(BINDING_API_PRE_RELEASE_VERSION_TYPE "${pyside_PRE_RELEASE_VERSION_TYPE}") set(BINDING_API_PRE_RELEASE_VERSION "${pyside_PRE_RELEASE_VERSION}") +set(pyside6_library_so_version "${SHIBOKEN_SO_VERSION}") # Detect if the Python interpreter is actually PyPy execute_process( @@ -97,7 +98,8 @@ if(QFP_QT_HOST_PATH) endif() endif() endif() -find_package(Qt6 REQUIRED COMPONENTS Core) +# Find QtGui private headers for exposing some QPA classes +find_package(Qt6 REQUIRED COMPONENTS Core CorePrivate Gui GuiPrivate) add_definitions(${Qt${QT_MAJOR_VERSION}Core_DEFINITIONS}) @@ -167,9 +169,12 @@ set (Qt${QT_MAJOR_VERSION}Widgets_FOUND "0") collect_essential_modules() collect_optional_modules() +# Additional (non-Qt) modules implemented in PySide only +set(PURE_PYTHON_MODULES Asyncio) + # Modules to be built unless specified by -DMODULES on command line if(NOT MODULES) - set(MODULES "${ALL_ESSENTIAL_MODULES};${ALL_OPTIONAL_MODULES}") + set(MODULES "${ALL_ESSENTIAL_MODULES};${ALL_OPTIONAL_MODULES};${PURE_PYTHON_MODULES}") set(required_modules ${ALL_ESSENTIAL_MODULES}) set(optional_modules ${ALL_OPTIONAL_MODULES}) else() @@ -180,6 +185,16 @@ list(REMOVE_ITEM MODULES ${SKIP_MODULES}) list(REMOVE_ITEM required_modules ${SKIP_MODULES}) list(REMOVE_ITEM optional_modules ${SKIP_MODULES}) +# Non-Qt modules must be removed before find_packages tries to locate them. +foreach(m IN LISTS PURE_PYTHON_MODULES) + set(DISABLE_Qt${m} 1) + if("Qt${m}" IN_LIST MODULES OR "${m}" IN_LIST MODULES) + set(DISABLE_Qt${m} 0) + endif() + list(FILTER MODULES EXCLUDE REGEX "^(Qt)?${m}$") + list(FILTER required_modules EXCLUDE REGEX "^(Qt)?${m}$") +endforeach() + find_package(Qt6 COMPONENTS ${required_modules} OPTIONAL_COMPONENTS ${optional_modules} @@ -192,7 +207,7 @@ remove_skipped_modules() # Mark all non-collected modules as disabled. This is used for disabling tests # that depend on the disabled modules. -foreach(m ${DISABLED_MODULES}) +foreach(m IN LISTS DISABLED_MODULES) set(DISABLE_Qt${m} 1) endforeach() @@ -216,9 +231,6 @@ endif() # no more supported: include(${QT_USE_FILE}) -# Configure OS support -check_os() - # Define supported Qt Version set(SUPPORTED_QT_VERSION "${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}.${QT_VERSION_PATCH}") @@ -255,8 +267,11 @@ set(GENERATOR_EXTRA_FLAGS use_protected_as_public_hack() # Build with Address sanitizer enabled if requested. This may break things, so use at your own risk. -if(SANITIZE_ADDRESS AND NOT MSVC) +if(SANITIZE_ADDRESS) setup_sanitize_address() endif() +if(SANITIZE_THREAD) + setup_sanitize_thread() +endif() find_package(Qt6 COMPONENTS Designer) diff --git a/sources/pyside6/doc/CMakeLists.txt b/sources/pyside6/doc/CMakeLists.txt index 8135c805..62a7a430 100644 --- a/sources/pyside6/doc/CMakeLists.txt +++ b/sources/pyside6/doc/CMakeLists.txt @@ -143,7 +143,7 @@ if (FULLDOCSBUILD) configure_file("pyside-config.qdocconf.in" "${config_docconf}" @ONLY) set(global_header "${pyside6_BINARY_DIR}/qdoc.h") - file(READ "${pyside6_BINARY_DIR}/pyside6_global.h" docHeaderContents) + file(READ "${CMAKE_CURRENT_LIST_DIR}/../PySide6/pyside6_global.h" docHeaderContents) file(WRITE ${global_header} "${docHeaderContents}") set(global_typesystem "${CMAKE_CURRENT_BINARY_DIR}/typesystem_doc.xml") @@ -221,7 +221,6 @@ if(DOC_OUTPUT_FORMAT STREQUAL "html") ${CMAKE_CURRENT_BINARY_DIR}/../../shiboken6/doc/html ${CMAKE_CURRENT_BINARY_DIR}/html/shiboken6 COMMENT "Copying Shiboken docs..." - DEPENDS "${DOC_DATA_DIR}/webxml/qtcore-index.webxml" VERBATIM) else() if(qhelpgenerator_binary) @@ -233,7 +232,6 @@ else() COMMAND ${python_executable} ${PATCH_QHP_SCRIPT} -p -v pyside6 ${QHP_FILE} COMMAND "${qhelpgenerator_binary}" ${QHP_FILE} COMMENT "Generating QCH from a QHP file..." - DEPENDS "${DOC_DATA_DIR}/webxml/qtcore-index.webxml" VERBATIM ) else() @@ -249,6 +247,8 @@ set(CODE_SNIPPET_ROOT "${CMAKE_CURRENT_BINARY_DIR}/${DOC_BASE_DIR}/codesnippets" if (FULLDOCSBUILD) shiboken_get_tool_shell_wrapper(shiboken tool_wrapper) +set(RHI_INCLUDE_DIR ${QT_INCLUDE_DIR}/QtGui/${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}.${QT_VERSION_PATCH}/QtGui) + add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${DOC_BASE_DIR}/PySide6/QtCore/index.rst" COMMAND ${tool_wrapper} @@ -256,7 +256,7 @@ add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${DOC_BASE_DIR}/PySide6/Q --generator-set=qtdoc ${global_header} --enable-pyside-extensions - --include-paths="${QT_INCLUDE_DIR}${PATH_SEP}${pyside6_SOURCE_DIR}${PATH_SEP}${TS_ROOT}" + --include-paths="${QT_INCLUDE_DIR}${PATH_SEP}${RHI_INCLUDE_DIR}${PATH_SEP}${pyside6_SOURCE_DIR}${PATH_SEP}${TS_ROOT}" --api-version=${SUPPORTED_QT_VERSION} --typesystem-paths="${QDOC_TYPESYSTEM_PATH}" --library-source-dir=${QT_SRC_DIR} @@ -268,7 +268,6 @@ add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${DOC_BASE_DIR}/PySide6/Q --additional-documentation=${CMAKE_CURRENT_BINARY_DIR}/${DOC_BASE_DIR}/additionaldocs.lst --inheritance-file=${ENV_INHERITANCE_FILE} ${global_typesystem} - WORKING_DIRECTORY ${${module}_SOURCE_DIR} COMMENT "Running generator to generate documentation...") endif() diff --git a/sources/pyside6/doc/_tags/android.rst b/sources/pyside6/doc/_tags/android.rst index 0fdf2041..d694f75f 100644 --- a/sources/pyside6/doc/_tags/android.rst +++ b/sources/pyside6/doc/_tags/android.rst @@ -15,9 +15,9 @@ My tags: Android ../examples/example_multimedia_camera.rst ../examples/example_qml_editingmodel.rst ../examples/example_qml_usingmodel.rst + ../examples/example_quick_customitems_painteditem.rst ../examples/example_quick_models_objectlistmodel.rst ../examples/example_quick_models_stringlistmodel.rst - ../examples/example_quick_painteditem.rst ../examples/example_quickcontrols_contactslist.rst ../examples/example_quickcontrols_gallery.rst ../examples/example_widgets_widgets_digitalclock.rst diff --git a/sources/pyside6/doc/additionaldocs.lst b/sources/pyside6/doc/additionaldocs.lst index 700647a7..8a580ea6 100644 --- a/sources/pyside6/doc/additionaldocs.lst +++ b/sources/pyside6/doc/additionaldocs.lst @@ -4,7 +4,6 @@ # A line enclosed in [] denotes a (relative) target directory [overviews] -qt3d/webxml/qt3d-changes-qt6.webxml qt3d/webxml/qt3d-cpp.webxml qt3d/webxml/qt3d-overview.webxml qt3d/webxml/qt3d-qml.webxml @@ -12,20 +11,14 @@ qt3d/webxml/qt3drender-framegraph.webxml qt3d/webxml/qt3drender-geometry.webxml qt3d/webxml/qt3drender-porting-to-rhi.webxml qt3d/webxml/qt3drender-protips.webxml -qtbluetooth/webxml/qtbluetooth-changes-qt6.webxml qtbluetooth/webxml/qtbluetooth-le-overview.webxml qtbluetooth/webxml/qtbluetooth-overview.webxml -qtcharts/webxml/qtcharts-changes-qt6.webxml -qtcharts/webxml/qtcharts-overview.webxml -qtconcurrent/webxml/concurrent-changes-qt6.webxml qtconcurrent/webxml/qtconcurrentfilter.webxml qtconcurrent/webxml/qtconcurrentmap.webxml qtconcurrent/webxml/qtconcurrentrun.webxml qtconcurrent/webxml/qtconcurrenttask.webxml qtcore/webxml/animation-overview.webxml qtcore/webxml/bindableproperties.webxml -qtcore/webxml/cbor.webxml -qtcore/webxml/containers.webxml qtcore/webxml/custom-types.webxml qtcore/webxml/datastreamformat.webxml qtcore/webxml/eventsandfilters.webxml @@ -33,104 +26,134 @@ qtcore/webxml/implicit-sharing.webxml qtcore/webxml/io-functions.webxml qtcore/webxml/ipc.webxml qtcore/webxml/java-style-iterators.webxml -qtcore/webxml/json.webxml qtcore/webxml/metaobjects.webxml -qtcore/webxml/objecttrees.webxml +qtcore/webxml/native-ipc-keys.webxml qtcore/webxml/object.webxml +qtcore/webxml/objecttrees.webxml qtcore/webxml/permissions.webxml -qtcore/webxml/plugins.webxml qtcore/webxml/properties.webxml -qtcore/webxml/qabstracteventdispatcher-timerinfo.webxml -qtcore/webxml/qadoptshareddatatag.webxml -qtcore/webxml/qcborerror.webxml -qtcore/webxml/qcborparsererror.webxml -qtcore/webxml/qcborstreamreader-stringresult.webxml -qtcore/webxml/qglobalstatic.webxml -qtcore/webxml/qhashseed.webxml -qtcore/webxml/qiterator.webxml -qtcore/webxml/qjsonparseerror.webxml -qtcore/webxml/qprocess-createprocessarguments.webxml -qtcore/webxml/qstaticplugin.webxml -qtcore/webxml/qtalgorithms.webxml +qtcore/webxml/qt-add-android-permission.webxml qtcore/webxml/qtcborcommon.webxml -qtcore/webxml/qtcore-changes-qt6.webxml +qtcore/webxml/qtdarwinhelpers.webxml +qtcore/webxml/qtdeprecationmarkers.webxml +qtcore/webxml/qtenvironmentvariables.webxml qtcore/webxml/qtglobal.webxml -qtcore/webxml/qtimezone-offsetdata.webxml +qtcore/webxml/qtlogging.webxml qtcore/webxml/qtmath.webxml qtcore/webxml/qtplugin.webxml +qtcore/webxml/qtresource.webxml +qtcore/webxml/qtserialization.webxml +qtcore/webxml/qtsystemdetection.webxml +qtcore/webxml/qttranslation.webxml +qtcore/webxml/qtversion.webxml +qtcore/webxml/qtversionchecks.webxml +qtcore/webxml/qtypeinfo.webxml qtcore/webxml/resources.webxml -qtcore/webxml/shared.webxml +qtcore/webxml/shared-memory.webxml qtcore/webxml/signalsandslots.webxml qtcore/webxml/timers.webxml -qtdbus/webxml/dbus-changes-qt6.webxml qtdbus/webxml/qdbusdeclaringsignals.webxml qtdbus/webxml/qdbusdeclaringslots.webxml qtdbus/webxml/qdbustypesystem.webxml qtdbus/webxml/qdbusviewer.webxml qtdbus/webxml/qdbusxml2cpp.webxml -qtdbus/webxml/qtdbus-cmake-qt-add-dbus-adaptor.webxml -qtdbus/webxml/qtdbus-cmake-qt-add-dbus-interfaces.webxml -qtdbus/webxml/qtdbus-cmake-qt-add-dbus-interface.webxml -qtdbus/webxml/qtdbus-cmake-qt-generate-dbus-interface.webxml +qtdbus/webxml/qtdbus-overview.webxml qtdbus/webxml/usingadaptors.webxml -qtdesigner/webxml/qtdesigner-components.webxml -qtdesigner/webxml/qtdesigner-manual.webxml +qtdoc/webxml/accessible-qwidget.webxml qtdoc/webxml/accessible.webxml +qtdoc/webxml/annotated.webxml qtdoc/webxml/appicon.webxml -qtdoc/webxml/create-your-first-applications.webxml -qtdoc/webxml/deployment.webxml +qtdoc/webxml/bughowto.webxml +qtdoc/webxml/classes.webxml +qtdoc/webxml/classesandfunctions.webxml qtdoc/webxml/desktop-integration.webxml -qtdoc/webxml/exceptionsafety.webxml +qtdoc/webxml/embedded-linux.webxml qtdoc/webxml/explore-qt.webxml +qtdoc/webxml/exportcontrols.webxml +qtdoc/webxml/fdl.webxml +qtdoc/webxml/functions.webxml +qtdoc/webxml/get-and-install-qt-cli.webxml qtdoc/webxml/get-and-install-qt.webxml +qtdoc/webxml/getting-sources-from-git.webxml qtdoc/webxml/gettingstarted.webxml +qtdoc/webxml/gpl.webxml qtdoc/webxml/highdpi.webxml -qtdoc/webxml/install-qt-design-studio.webxml +qtdoc/webxml/i18n-plural-rules.webxml +qtdoc/webxml/i18n-source-translation.webxml +qtdoc/webxml/index.webxml +qtdoc/webxml/inputs-linux-device.webxml +qtdoc/webxml/ipc-overview.webxml qtdoc/webxml/known-issues.webxml +qtdoc/webxml/lgpl.webxml +qtdoc/webxml/linux-issues.webxml +qtdoc/webxml/linux-requirements.webxml +qtdoc/webxml/linux.webxml +qtdoc/webxml/localization.webxml +qtdoc/webxml/macos-issues.webxml +qtdoc/webxml/macos.webxml qtdoc/webxml/mobiledevelopment.webxml +qtdoc/webxml/modulechanges.webxml +qtdoc/webxml/modules-cpp.webxml +qtdoc/webxml/modules-qml.webxml +qtdoc/webxml/namespaces.webxml qtdoc/webxml/overviews-main.webxml -qtdoc/webxml/plugins-howto.webxml -qtdoc/webxml/qmlapplications.webxml -qtdoc/webxml/qml-codingconventions.webxml -qtdoc/webxml/qmlfirststeps.webxml -qtdoc/webxml/qml-glossary.webxml +qtdoc/webxml/overviews.webxml +qtdoc/webxml/porting-qt3d-to-qtquick3d.webxml +qtdoc/webxml/porting-to-android.webxml +qtdoc/webxml/portingguide.webxml +qtdoc/webxml/qmltypes.webxml +qtdoc/webxml/qmlvaluetypes.webxml +qtdoc/webxml/qt-additional-modules.webxml +qtdoc/webxml/qt-edu-for-designers.webxml +qtdoc/webxml/qt-edu-for-developers.webxml +qtdoc/webxml/qt-edu-mcu.webxml +qtdoc/webxml/qt-edu-raspberry-pi.webxml +qtdoc/webxml/qt-edu-resources.webxml +qtdoc/webxml/qt-embedded-fonts.webxml +qtdoc/webxml/qt-embedded-kmap2qmap.webxml +qtdoc/webxml/qt-for-education.webxml qtdoc/webxml/qt-intro.webxml -qtdoc/webxml/qtlanguages.webxml -qtdoc/webxml/qtquick-debugging.webxml -qtdoc/webxml/qtquick-performance.webxml -qtdoc/webxml/qtquick-qml-runtime.webxml -qtdoc/webxml/qtquick-usecase-animations.webxml -qtdoc/webxml/qtquick-usecase-integratingjs.webxml -qtdoc/webxml/qtquick-usecase-layouts.webxml -qtdoc/webxml/qtquick-usecase-styling.webxml -qtdoc/webxml/qtquick-usecase-text.webxml -qtdoc/webxml/qtquick-usecase-userinput.webxml -qtdoc/webxml/qtquick-usecase-visual.webxml +qtdoc/webxml/qt-online-installation.webxml qtdoc/webxml/qt-releases.webxml +qtdoc/webxml/qt-tools-utilities.webxml +qtdoc/webxml/qtlanguages.webxml +qtdoc/webxml/qtmodules.webxml qtdoc/webxml/qundo.webxml qtdoc/webxml/rcc.webxml +qtdoc/webxml/reference-overview.webxml qtdoc/webxml/restoring-geometry.webxml qtdoc/webxml/scalability.webxml qtdoc/webxml/security.webxml qtdoc/webxml/session.webxml qtdoc/webxml/sharedlibrary.webxml +qtdoc/webxml/signalsandslots-syntaxes.webxml qtdoc/webxml/solutions-for-application-development.webxml -qtdoc/webxml/solutions-for-ui-design.webxml +qtdoc/webxml/supported-platforms.webxml qtdoc/webxml/testing-and-debugging.webxml -qtdoc/webxml/thread-basics.webxml +qtdoc/webxml/threads-modules.webxml +qtdoc/webxml/threads-qobject.webxml +qtdoc/webxml/threads-reentrancy.webxml +qtdoc/webxml/threads-synchronizing.webxml +qtdoc/webxml/threads-technologies.webxml qtdoc/webxml/threads.webxml qtdoc/webxml/tools-for-qt-quick-uis.webxml -qtdoc/webxml/tools-for-qt-widget-based-uis.webxml qtdoc/webxml/topics-app-development.webxml qtdoc/webxml/topics-core.webxml qtdoc/webxml/topics-data-io.webxml qtdoc/webxml/topics-graphics.webxml +qtdoc/webxml/topics-graphics2d.webxml qtdoc/webxml/topics-network-connectivity.webxml qtdoc/webxml/topics-ui.webxml +qtdoc/webxml/topics-vectorimageformats.webxml +qtdoc/webxml/trademarks.webxml qtdoc/webxml/uic.webxml qtdoc/webxml/unicode.webxml -qtdoc/webxml/untrusteddata.webxml qtdoc/webxml/wayland-and-qt.webxml +qtdoc/webxml/wayland-requirements.webxml +qtdoc/webxml/windows-graphics.webxml +qtdoc/webxml/windows-issues.webxml +qtdoc/webxml/windows.webxml +qtdoc/webxml/xml-processing.webxml qtgraphs/webxml/qtgraphs-and-qtquick3d-integration-guide.webxml qtgraphs/webxml/qtgraphs-configure-options.webxml qtgraphs/webxml/qtgraphs-data-handling.webxml @@ -143,18 +166,10 @@ qtgraphs/webxml/qtgraphs-overview-3d.webxml qtgraphs/webxml/qtgraphs-overview-theme.webxml qtgui/webxml/coordsys.webxml qtgui/webxml/dnd.webxml -qtgui/webxml/gui-changes-qt6.webxml qtgui/webxml/paintsystem-devices.webxml qtgui/webxml/paintsystem-drawing.webxml qtgui/webxml/paintsystem-images.webxml qtgui/webxml/paintsystem.webxml -qtgui/webxml/qabstracttextdocumentlayout-paintcontext.webxml -qtgui/webxml/qabstracttextdocumentlayout-selection.webxml -qtgui/webxml/qaccessible-state.webxml -qtgui/webxml/qiconengine-scaledpixmapargument.webxml -qtgui/webxml/qpageranges-range.webxml -qtgui/webxml/qtextlayout-formatrange.webxml -qtgui/webxml/qtextoption-tab.webxml qtgui/webxml/qtgui-overview.webxml qtgui/webxml/richtext-advanced-processing.webxml qtgui/webxml/richtext-common-tasks.webxml @@ -163,65 +178,108 @@ qtgui/webxml/richtext-html-subset.webxml qtgui/webxml/richtext-layouts.webxml qtgui/webxml/richtext-structure.webxml qtgui/webxml/richtext.webxml -qthelp/webxml/helpsystem.webxml -qthelp/webxml/qhelplink.webxml qthelp/webxml/qthelp-framework.webxml qthelp/webxml/qthelpproject.webxml -qthttpserver/webxml/qthttpserver-colorpalette-apibehavior-h.webxml -qthttpserver/webxml/qthttpserver-colorpalette-colorpalette-pro.webxml -qthttpserver/webxml/qthttpserver-colorpalette-main-cpp.webxml -qthttpserver/webxml/qthttpserver-colorpalette-types-h.webxml -qthttpserver/webxml/qthttpserver-colorpalette-utils-h.webxml qthttpserver/webxml/qthttpserver-logging.webxml +qtlocation/webxml/location-maps-cpp.webxml +qtlocation/webxml/location-maps-qml.webxml qtlocation/webxml/location-places-backend.webxml qtlocation/webxml/location-plugin-itemsoverlay.webxml qtlocation/webxml/location-plugin-osm.webxml qtlocation/webxml/qml-location5-maps.webxml +qtlocation/webxml/qtlocation-cpp.webxml qtlocation/webxml/qtlocation-geoservices.webxml qtmultimedia/webxml/advanced-ffmpeg-configuration.webxml qtmultimedia/webxml/audiooverview.webxml qtmultimedia/webxml/cameraoverview.webxml qtmultimedia/webxml/multimediaoverview.webxml -qtmultimedia/webxml/qmediatimerange-interval.webxml qtmultimedia/webxml/qt-add-ios-ffmpeg-libraries.webxml qtmultimedia/webxml/qtmultimedia-apple.webxml -qtmultimedia/webxml/qtmultimedia-changes-qt6.webxml +qtmultimedia/webxml/qtmultimedia-ffmpeg-stubs.webxml qtmultimedia/webxml/qtmultimedia-gstreamer.webxml +qtmultimedia/webxml/qtmultimedia-linux.webxml qtmultimedia/webxml/qtmultimedia-modules.webxml -qtmultimedia/webxml/qtmultimedia-wayland.webxml +qtmultimedia/webxml/qtmultimedia-wasm.webxml qtmultimedia/webxml/qtmultimedia-windows.webxml qtmultimedia/webxml/videooverview.webxml -qtnetworkauth/webxml/oauth-http-method-alternatives.webxml -qtnetwork/webxml/network-changes-qt6.webxml -qtnetwork/webxml/qdtlsclientverifier-generatorparameters.webxml qtnetwork/webxml/qtnetwork-programming.webxml qtnetwork/webxml/ssl.webxml +qtnetworkauth/webxml/oauth-http-method-alternatives.webxml +qtnetworkauth/webxml/qt-oauth2-browsersupport.webxml +qtnetworkauth/webxml/qt-oauth2-overview.webxml +qtnetworkauth/webxml/qtnetworkauth-security.webxml qtnfc/webxml/nfc-android.webxml -qtnfc/webxml/qndeffilter-record.webxml -qtnfc/webxml/qtnfc-changes-qt6.webxml qtnfc/webxml/qtnfc-features.webxml qtnfc/webxml/qtnfc-overview.webxml qtnfc/webxml/qtnfc-pcsc.webxml -qtopengl/webxml/opengl-changes-qt6.webxml -qtpositioning/webxml/positioning-cpp-qml.webxml +qtpdf/webxml/qtpdf-platformnotes.webxml +qtpositioning/webxml/location-positioning-qml.webxml qtpositioning/webxml/position-plugin-android.webxml qtpositioning/webxml/position-plugin-geoclue2.webxml qtpositioning/webxml/position-plugin-gypsy.webxml qtpositioning/webxml/position-plugin-nmea.webxml +qtpositioning/webxml/positioning-cpp-qml.webxml qtpositioning/webxml/qtpositioning-android.webxml -qtpositioning/webxml/qtpositioning-changes-qt6.webxml qtpositioning/webxml/qtpositioning-ios.webxml qtpositioning/webxml/qtpositioning-plugins.webxml qtprintsupport/webxml/pdf-licensing.webxml -qtprintsupport/webxml/printsupport-changes-qt6.webxml -qtqml/webxml/qml-changes-qt6.webxml +qtqml/webxml/qml-singleton.webxml qtqml/webxml/qmldiskcache.webxml +qtqml/webxml/qmllint-warnings-and-errors-access-singleton-via-object.webxml +qtqml/webxml/qmllint-warnings-and-errors-alias-cycle.webxml +qtqml/webxml/qmllint-warnings-and-errors-assignment-in-condition.webxml +qtqml/webxml/qmllint-warnings-and-errors-attached-property-reuse.webxml +qtqml/webxml/qmllint-warnings-and-errors-comma.webxml +qtqml/webxml/qmllint-warnings-and-errors-confusing-minuses.webxml +qtqml/webxml/qmllint-warnings-and-errors-confusing-pluses.webxml +qtqml/webxml/qmllint-warnings-and-errors-deprecated.webxml +qtqml/webxml/qmllint-warnings-and-errors-duplicate-enum-entries.webxml +qtqml/webxml/qmllint-warnings-and-errors-duplicate-import.webxml +qtqml/webxml/qmllint-warnings-and-errors-duplicate-inline-components.webxml +qtqml/webxml/qmllint-warnings-and-errors-duplicate-property-binding.webxml +qtqml/webxml/qmllint-warnings-and-errors-duplicated-name.webxml +qtqml/webxml/qmllint-warnings-and-errors-enum-entry-matches-enum.webxml +qtqml/webxml/qmllint-warnings-and-errors-import.webxml +qtqml/webxml/qmllint-warnings-and-errors-incompatible-type.webxml +qtqml/webxml/qmllint-warnings-and-errors-inheritance-cycle.webxml +qtqml/webxml/qmllint-warnings-and-errors-invalid-lint-directive.webxml +qtqml/webxml/qmllint-warnings-and-errors-literal-constructor.webxml +qtqml/webxml/qmllint-warnings-and-errors-missing-enum-entry.webxml +qtqml/webxml/qmllint-warnings-and-errors-missing-property.webxml +qtqml/webxml/qmllint-warnings-and-errors-missing-type.webxml +qtqml/webxml/qmllint-warnings-and-errors-multiline-strings.webxml +qtqml/webxml/qmllint-warnings-and-errors-non-list-property.webxml +qtqml/webxml/qmllint-warnings-and-errors-non-root-enum.webxml +qtqml/webxml/qmllint-warnings-and-errors-quick-attached-property-type.webxml +qtqml/webxml/qmllint-warnings-and-errors-read-only-property.webxml +qtqml/webxml/qmllint-warnings-and-errors-recursion-depth-errors.webxml +qtqml/webxml/qmllint-warnings-and-errors-redundant-optional-chaining.webxml +qtqml/webxml/qmllint-warnings-and-errors-required.webxml +qtqml/webxml/qmllint-warnings-and-errors-restricted-type.webxml +qtqml/webxml/qmllint-warnings-and-errors-signal-handler-parameters.webxml +qtqml/webxml/qmllint-warnings-and-errors-syntax-duplicate-ids.webxml +qtqml/webxml/qmllint-warnings-and-errors-syntax-id-quotation.webxml +qtqml/webxml/qmllint-warnings-and-errors-syntax.webxml +qtqml/webxml/qmllint-warnings-and-errors-top-level-component.webxml +qtqml/webxml/qmllint-warnings-and-errors-uncreatable-type.webxml +qtqml/webxml/qmllint-warnings-and-errors-unqualified.webxml +qtqml/webxml/qmllint-warnings-and-errors-unresolved-alias.webxml +qtqml/webxml/qmllint-warnings-and-errors-unresolved-type.webxml +qtqml/webxml/qmllint-warnings-and-errors-unterminated-case.webxml +qtqml/webxml/qmllint-warnings-and-errors-unused-imports.webxml +qtqml/webxml/qmllint-warnings-and-errors-use-proper-function.webxml +qtqml/webxml/qmllint-warnings-and-errors-var-used-before-declaration.webxml +qtqml/webxml/qmllint-warnings-and-errors-void.webxml +qtqml/webxml/qmllint-warnings-and-errors-with.webxml qtqml/webxml/qmlreference.webxml -qtqml/webxml/qml-singleton.webxml -qtqml/webxml/qqmlcontext-propertypair.webxml +qtqml/webxml/qqml-h.webxml +qtqml/webxml/qqmlintegration-h.webxml qtqml/webxml/qt-add-qml-plugin.webxml qtqml/webxml/qt-import-qml-plugins.webxml +qtqml/webxml/qt-target-qml-sources.webxml +qtqml/webxml/qt6-modernize-qml-modules.webxml qtqml/webxml/qtjavascript.webxml +qtqml/webxml/qtqml-cppclasses-topic.webxml qtqml/webxml/qtqml-documents-definetypes.webxml qtqml/webxml/qtqml-documents-networktransparency.webxml qtqml/webxml/qtqml-documents-scope.webxml @@ -247,27 +305,103 @@ qtqml/webxml/qtqml-syntax-imports.webxml qtqml/webxml/qtqml-syntax-objectattributes.webxml qtqml/webxml/qtqml-syntax-propertybinding.webxml qtqml/webxml/qtqml-syntax-signals.webxml +qtqml/webxml/qtqml-tool-qmlcachegen.webxml +qtqml/webxml/qtqml-tooling-qml.webxml +qtqml/webxml/qtqml-tooling-qmlformat.webxml +qtqml/webxml/qtqml-tooling-qmlimportscanner.webxml +qtqml/webxml/qtqml-tooling-qmllint.webxml +qtqml/webxml/qtqml-tooling-qmlls.webxml +qtqml/webxml/qtqml-tooling-qmlpreview.webxml +qtqml/webxml/qtqml-tooling-qmlprofiler.webxml +qtqml/webxml/qtqml-tooling-qmltyperegistrar.webxml +qtqml/webxml/qtqml-tooling-svgtoqml.webxml qtqml/webxml/qtqml-typesystem-basictypes.webxml +qtqml/webxml/qtqml-typesystem-enumerations.webxml qtqml/webxml/qtqml-typesystem-namespaces.webxml qtqml/webxml/qtqml-typesystem-objecttypes.webxml qtqml/webxml/qtqml-typesystem-references.webxml qtqml/webxml/qtqml-typesystem-sequencetypes.webxml qtqml/webxml/qtqml-typesystem-topic.webxml qtqml/webxml/qtqml-typesystem-valuetypes.webxml -qtqml/webxml/qt-target-qml-sources.webxml +qtqml/webxml/qtquick-debugging.webxml +qtqml/webxml/qtquick-qml-runtime.webxml +qtquick/webxml/qml-advtutorial.webxml +qtquick/webxml/qml-codingconventions.webxml +qtquick/webxml/qml-dynamicview-tutorial.webxml +qtquick/webxml/qml-glossary.webxml +qtquick/webxml/qml-tutorial.webxml +qtquick/webxml/qml-tutorial1.webxml +qtquick/webxml/qml-tutorial2.webxml +qtquick/webxml/qml-tutorial3.webxml +qtquick/webxml/qmlapplications.webxml +qtquick/webxml/qmlfirststeps.webxml +qtquick/webxml/qtquick-android-classes.webxml +qtquick/webxml/qtquick-bestpractices.webxml +qtquick/webxml/qtquick-codesamples.webxml +qtquick/webxml/qtquick-convenience-topic.webxml +qtquick/webxml/qtquick-cppextensionpoints.webxml +qtquick/webxml/qtquick-effects-particles.webxml +qtquick/webxml/qtquick-effects-sprites.webxml +qtquick/webxml/qtquick-effects-topic.webxml +qtquick/webxml/qtquick-effects-transformations.webxml +qtquick/webxml/qtquick-for-android-fragments.webxml +qtquick/webxml/qtquick-for-android.webxml +qtquick/webxml/qtquick-how-tos.webxml +qtquick/webxml/qtquick-input-focus.webxml +qtquick/webxml/qtquick-input-mouseevents.webxml +qtquick/webxml/qtquick-input-textinput.webxml +qtquick/webxml/qtquick-input-topic.webxml +qtquick/webxml/qtquick-modelviewsdata-cppmodels.webxml +qtquick/webxml/qtquick-modelviewsdata-modelview.webxml +qtquick/webxml/qtquick-modelviewsdata-sqlmodels.webxml +qtquick/webxml/qtquick-modelviewsdata-topic.webxml +qtquick/webxml/qtquick-particles-performance.webxml +qtquick/webxml/qtquick-performance.webxml +qtquick/webxml/qtquick-positioning-anchors.webxml +qtquick/webxml/qtquick-positioning-layouts.webxml +qtquick/webxml/qtquick-positioning-righttoleft.webxml +qtquick/webxml/qtquick-positioning-topic.webxml +qtquick/webxml/qtquick-statesanimations-animations.webxml +qtquick/webxml/qtquick-statesanimations-behaviors.webxml +qtquick/webxml/qtquick-statesanimations-states.webxml +qtquick/webxml/qtquick-statesanimations-topic.webxml +qtquick/webxml/qtquick-tools-and-utilities.webxml +qtquick/webxml/qtquick-usecase-animations.webxml +qtquick/webxml/qtquick-usecase-integratingjs.webxml +qtquick/webxml/qtquick-usecase-layouts.webxml +qtquick/webxml/qtquick-usecase-text.webxml +qtquick/webxml/qtquick-usecase-userinput.webxml +qtquick/webxml/qtquick-usecase-visual.webxml +qtquick/webxml/qtquick-visualcanvas-adaptations-openvg.webxml +qtquick/webxml/qtquick-visualcanvas-adaptations-software.webxml +qtquick/webxml/qtquick-visualcanvas-adaptations.webxml +qtquick/webxml/qtquick-visualcanvas-coordinates.webxml +qtquick/webxml/qtquick-visualcanvas-scenegraph-renderer.webxml +qtquick/webxml/qtquick-visualcanvas-scenegraph.webxml +qtquick/webxml/qtquick-visualcanvas-topic.webxml +qtquick/webxml/qtquick-visualcanvas-visualparent.webxml +qtquick/webxml/qtquick-visualtypes-topic.webxml +qtquick/webxml/qtquicklayouts-overview.webxml +qtquick/webxml/qtquicklayouts-responsive.webxml +qtquick/webxml/scalability.webxml +qtquick3d/webxml/qt-add-lightprobe-images.webxml +qtquick3d/webxml/qt-add-materials.webxml +qtquick3d/webxml/qt-quick-3d-xr.webxml qtquick3d/webxml/qt3dxr-multiview.webxml qtquick3d/webxml/qt3dxr-pcvr.webxml qtquick3d/webxml/qt3dxr-quest-quick-start.webxml qtquick3d/webxml/qt3dxr-quick-start-guide-applevisionpro.webxml qtquick3d/webxml/qt3dxr-supported-openxr-extensions.webxml qtquick3d/webxml/qt3dxr-supported-platforms.webxml -qtquick3d/webxml/qtaa-toc.webxml qtquick3d/webxml/qtquick3d-2d.webxml qtquick3d/webxml/qtquick3d-architecture.webxml qtquick3d/webxml/qtquick3d-custom.webxml qtquick3d/webxml/qtquick3d-lod.webxml qtquick3d/webxml/qtquick3d-requirements.webxml -qtquick3d/webxml/qt-quick-3d-xr.webxml +qtquick3d/webxml/qtquick3d-tool-balsam.webxml +qtquick3d/webxml/qtquick3d-tool-instancer.webxml +qtquick3d/webxml/qtquick3d-tool-materialeditor.webxml +qtquick3d/webxml/qtquick3d-tool-shadergen.webxml qtquick3d/webxml/qtxr-locomotion.webxml qtquick3d/webxml/quick3d-asset-conditioning-3d-assets.webxml qtquick3d/webxml/quick3d-asset-conditioning-anti-aliasing.webxml @@ -285,7 +419,6 @@ qtquick3d/webxml/quick3d-shadow-mapping.webxml qtquick3d/webxml/quick3d-vertex-skinning.webxml qtquickcontrols/webxml/qtquickcontrols-basic.webxml qtquickcontrols/webxml/qtquickcontrols-buttons.webxml -qtquickcontrols/webxml/qtquickcontrols-changes-qt6.webxml qtquickcontrols/webxml/qtquickcontrols-configuration.webxml qtquickcontrols/webxml/qtquickcontrols-containers.webxml qtquickcontrols/webxml/qtquickcontrols-customize.webxml @@ -311,64 +444,8 @@ qtquickcontrols/webxml/qtquickcontrols-popups.webxml qtquickcontrols/webxml/qtquickcontrols-separators.webxml qtquickcontrols/webxml/qtquickcontrols-styles.webxml qtquickcontrols/webxml/qtquickcontrols-universal.webxml +qtquickcontrols/webxml/qtquickcontrols-versioning.webxml qtquickcontrols/webxml/qtquickcontrols-windows.webxml -qtquick/webxml/qml-advtutorial.webxml -qtquick/webxml/qml-dynamicview-tutorial.webxml -qtquick/webxml/qml-tutorial1.webxml -qtquick/webxml/qml-tutorial2.webxml -qtquick/webxml/qml-tutorial3.webxml -qtquick/webxml/qml-tutorial.webxml -qtquick/webxml/qquickitem-itemchangedata.webxml -qtquick/webxml/qsggeometry-attribute.webxml -qtquick/webxml/qsggeometry-coloredpoint2d.webxml -qtquick/webxml/qsggeometry-point2d.webxml -qtquick/webxml/qsggeometry-texturedpoint2d.webxml -qtquick/webxml/qsgmaterialshader-graphicspipelinestate.webxml -qtquick/webxml/qsgmaterialtype.webxml -qtquick/webxml/qtquick-android-classes.webxml -qtquick/webxml/qtquick-bestpractices.webxml -qtquick/webxml/qtquick-codesamples.webxml -qtquick/webxml/qtquick-convenience-topic.webxml -qtquick/webxml/qtquick-cppextensionpoints.webxml -qtquick/webxml/qtquick-effects-particles.webxml -qtquick/webxml/qtquick-effects-sprites.webxml -qtquick/webxml/qtquick-effects-topic.webxml -qtquick/webxml/qtquick-effects-transformations.webxml -qtquick/webxml/qtquick-how-tos.webxml -qtquick/webxml/qtquick-input-focus.webxml -qtquick/webxml/qtquick-input-mouseevents.webxml -qtquick/webxml/qtquick-input-textinput.webxml -qtquick/webxml/qtquick-input-topic.webxml -qtquick/webxml/qtquicklayouts-overview.webxml -qtquick/webxml/qtquicklayouts-responsive.webxml -qtquick/webxml/qtquick-modelviewsdata-cppmodels.webxml -qtquick/webxml/qtquick-modelviewsdata-modelview.webxml -qtquick/webxml/qtquick-modelviewsdata-topic.webxml -qtquick/webxml/qtquick-particles-performance.webxml -qtquick/webxml/qtquick-positioning-anchors.webxml -qtquick/webxml/qtquick-positioning-layouts.webxml -qtquick/webxml/qtquick-positioning-righttoleft.webxml -qtquick/webxml/qtquick-positioning-topic.webxml -qtquick/webxml/qtquick-statesanimations-animations.webxml -qtquick/webxml/qtquick-statesanimations-behaviors.webxml -qtquick/webxml/qtquick-statesanimations-states.webxml -qtquick/webxml/qtquick-statesanimations-topic.webxml -qtquick/webxml/qtquick-tools-and-utilities.webxml -qtquick/webxml/qtquickview-android-class.webxml -qtquick/webxml/qtquick-visualcanvas-adaptations-openvg.webxml -qtquick/webxml/qtquick-visualcanvas-adaptations-software.webxml -qtquick/webxml/qtquick-visualcanvas-adaptations.webxml -qtquick/webxml/qtquick-visualcanvas-coordinates.webxml -qtquick/webxml/qtquick-visualcanvas-scenegraph-renderer.webxml -qtquick/webxml/qtquick-visualcanvas-scenegraph.webxml -qtquick/webxml/qtquick-visualcanvas-topic.webxml -qtquick/webxml/qtquick-visualcanvas-visualparent.webxml -qtquick/webxml/qtquick-visualtypes-topic.webxml -qtquick/webxml/quick-changes-qt6.webxml -qtremoteobjects/webxml/qtremoteobjects-cmake-qt-add-repc-merged.webxml -qtremoteobjects/webxml/qtremoteobjects-cmake-qt-add-repc-replicas.webxml -qtremoteobjects/webxml/qtremoteobjects-cmake-qt-add-repc-sources.webxml -qtremoteobjects/webxml/qtremoteobjects-cmake-qt-rep-from-headers.webxml qtremoteobjects/webxml/qtremoteobjects-compatibility.webxml qtremoteobjects/webxml/qtremoteobjects-custom-transport.webxml qtremoteobjects/webxml/qtremoteobjects-external-schemas.webxml @@ -380,10 +457,7 @@ qtremoteobjects/webxml/qtremoteobjects-repc.webxml qtremoteobjects/webxml/qtremoteobjects-replica.webxml qtremoteobjects/webxml/qtremoteobjects-source.webxml qtremoteobjects/webxml/qtremoteobjects-troubleshooting.webxml -qtremoteobjects/webxml/remoteobjects-changes-qt6.webxml qtscxml/webxml/qscxmlc.webxml -qtscxml/webxml/qtscxml-changes-qt6.webxml -qtscxml/webxml/qtscxml-cmake-qt-add-statecharts.webxml qtscxml/webxml/qtscxml-instantiating-state-machines.webxml qtscxml/webxml/qtscxml-overview.webxml qtscxml/webxml/qtscxml-scxml-compliance.webxml @@ -392,8 +466,6 @@ qtsensors/webxml/creating-a-sensor-plugin.webxml qtsensors/webxml/determining-the-default-sensor-for-a-type.webxml qtsensors/webxml/dynamic-sensor-backend-registration.webxml qtsensors/webxml/genericbackend.webxml -qtsensors/webxml/qoutputrange.webxml -qtsensors/webxml/qtsensors-changes-qt6.webxml qtsensors/webxml/qtsensors-cpp.webxml qtsensors/webxml/qtsensors-tutorial.webxml qtsensors/webxml/senorfwbackend.webxml @@ -407,9 +479,8 @@ qtserialbus/webxml/qtserialbus-systeccan-overview.webxml qtserialbus/webxml/qtserialbus-tinycan-overview.webxml qtserialbus/webxml/qtserialbus-vectorcan-overview.webxml qtserialbus/webxml/qtserialbus-virtualcan-overview.webxml -qtserialport/webxml/qtserialport-changes-qt6.webxml +qtspatialaudio/webxml/qtspatialaudio-modules.webxml qtspatialaudio/webxml/spatialaudiooverview.webxml -qtsql/webxml/qtsql-changes-qt6.webxml qtsql/webxml/sql-connecting.webxml qtsql/webxml/sql-driver.webxml qtsql/webxml/sql-forms.webxml @@ -418,27 +489,26 @@ qtsql/webxml/sql-presenting.webxml qtsql/webxml/sql-programming.webxml qtsql/webxml/sql-sqlstatements.webxml qtsql/webxml/sql-types.webxml -qtsvg/webxml/qtsvg-changes-qt6.webxml +qtstatemachine/webxml/qmlstatemachine-qml-guide.webxml +qtstatemachine/webxml/qtstatemachine-cpp-guide.webxml +qtstatemachine/webxml/qtstatemachine-overview.webxml qtsvg/webxml/svgextensions.webxml qtsvg/webxml/svgrendering.webxml qttestlib/webxml/qtest-overview.webxml qttestlib/webxml/qtest-tutorial.webxml qttestlib/webxml/qttest-best-practices-qdoc.webxml qttestlib/webxml/qttestlib-tutorial6.webxml -qttestlib/webxml/testlib-changes-qt6.webxml -qttexttospeech/webxml/qttexttospeech-changes-qt6.webxml qttexttospeech/webxml/qttexttospeech-engines.webxml -qtwebchannel/webxml/qtwebchannel-changes-qt6.webxml qtwebchannel/webxml/qtwebchannel-javascript.webxml -qtwebengine/webxml/qtwebengine-changes-qt6.webxml +qtwebengine/webxml/qt-add-webengine-dictionary.webxml qtwebengine/webxml/qtwebengine-features.webxml +qtwebengine/webxml/qtwebengine-modules.webxml qtwebengine/webxml/qtwebengine-overview.webxml -qtwebengine/webxml/qtwebengine-platform-notes.webxml +qtwebengine/webxml/qtwebengine-security.webxml +qtwebengine/webxml/qtwebenginecoreglobal-h.webxml qtwebengine/webxml/qtwebenginewidgets-qtwebkitportingguide.webxml -qtwebengine/webxml/qwebenginecookiestore-filterrequest.webxml qtwebsockets/webxml/echoclient.webxml qtwebsockets/webxml/echoserver.webxml -qtwebsockets/webxml/qtwebsockets-changes-qt6.webxml qtwebsockets/webxml/qtwebsockets-testing.webxml qtwebsockets/webxml/websockets-overview.webxml qtwidgets/webxml/application-windows.webxml @@ -452,9 +522,8 @@ qtwidgets/webxml/mainwindow.webxml qtwidgets/webxml/model-view-programming.webxml qtwidgets/webxml/modelview.webxml qtwidgets/webxml/qdrawutil-h.webxml -qtwidgets/webxml/qformlayout-takerowresult.webxml -qtwidgets/webxml/qtextedit-extraselection.webxml -qtwidgets/webxml/qtilerules.webxml +qtwidgets/webxml/qt-add-ui.webxml +qtwidgets/webxml/qt-wrap-ui.webxml qtwidgets/webxml/qwidget-styling.webxml qtwidgets/webxml/style-reference.webxml qtwidgets/webxml/stylesheet-customizing.webxml @@ -463,14 +532,13 @@ qtwidgets/webxml/stylesheet-reference.webxml qtwidgets/webxml/stylesheet-syntax.webxml qtwidgets/webxml/stylesheet.webxml qtwidgets/webxml/widget-classes.webxml -qtwidgets/webxml/widgets-changes-qt6.webxml +qtwidgets/webxml/widget-tutorials.webxml +qtwidgets/webxml/widgets-getting-started.webxml qtwidgets/webxml/widgets-tutorial.webxml -qtxml/webxml/xml-changes-qt6.webxml -qtxml/webxml/xml-dom-tml.webxml +qtxml/webxml/xml-dom.webxml qtxml/webxml/xml-namespaces.webxml qtxml/webxml/xml-processing.webxml qtxml/webxml/xml-streaming.webxml -qtxml/webxml/xml-tools.webxml # WebXML files (lists of classes grouped by function/curated) [groups] diff --git a/sources/pyside6/doc/building_from_source/index.rst b/sources/pyside6/doc/building_from_source/index.rst index 33765167..a2014372 100644 --- a/sources/pyside6/doc/building_from_source/index.rst +++ b/sources/pyside6/doc/building_from_source/index.rst @@ -35,9 +35,6 @@ website. Prebuilt versions for each OS can be `downloaded here`_. * Check the `Supported Platforms of Qt`_ -.. _downloaded here: https://download.qt.io/development_releases/prebuilt/libclang/ -.. _`Supported Platforms of Qt` : https://doc.qt.io/qt-6/supported-platforms.html - Guides per platform ------------------- @@ -487,7 +484,7 @@ The target executes several steps: #. ``sphinx`` is run to produce the documentation in HTML format. Re-running the command will not execute step 1 unless the file -``qdoc-output/webxml/qtcore-index.webxml`` is removed from the build tree. +``qdoc-output/qtcore/webxml/qtcore-index.webxml`` is removed from the build tree. Similarly, step 2 will not be executed unless the file ``base/PySide6/QtCore/index.rst`` is removed. @@ -570,3 +567,5 @@ A set of tools can be found under the ``tools/`` directory inside the ``pyside-s .. _`wiki page`: https://wiki.qt.io/Qt_for_Python_Missing_Bindings .. _BeautifulSoup: https://www.crummy.com/software/BeautifulSoup/ .. _`CMake Unity Build Mode` : https://cmake.org/cmake/help/latest/prop_tgt/UNITY_BUILD.html +.. _downloaded here: https://download.qt.io/development_releases/prebuilt/libclang/ +.. _`Supported Platforms of Qt` : https://doc.qt.io/qt-6/supported-platforms.html diff --git a/sources/pyside6/doc/building_from_source/linux.rst b/sources/pyside6/doc/building_from_source/linux.rst index 1e59006e..80d56f44 100644 --- a/sources/pyside6/doc/building_from_source/linux.rst +++ b/sources/pyside6/doc/building_from_source/linux.rst @@ -41,12 +41,12 @@ Setting up CLANG If you don't have libclang already in your system, you can download from the Qt servers:: - wget https://download.qt.io/development_releases/prebuilt/libclang/libclang-release_18.1.5-based-linux-Rhel8.6-gcc10.3-x86_64.7z + wget https://download.qt.io/development_releases/prebuilt/libclang/libclang-release_20.1.3-based-linux-Rhel8.8-gcc10.3-x86_64.7z Extract the files, and leave it in any desired path, and set the environment variable required:: - 7z x libclang-release_18.1.5-based-linux-Rhel8.6-gcc10.3-x86_64.7z + 7z x libclang-release_20.1.3-based-linux-Rhel8.8-gcc10.3-x86_64.7z export LLVM_INSTALL_DIR=$PWD/libclang Getting the source diff --git a/sources/pyside6/doc/building_from_source/macOS.rst b/sources/pyside6/doc/building_from_source/macOS.rst index 67a3d48e..0e10d6cf 100644 --- a/sources/pyside6/doc/building_from_source/macOS.rst +++ b/sources/pyside6/doc/building_from_source/macOS.rst @@ -45,12 +45,12 @@ Setting up CLANG If you don't have `libclang` already in your system, you can download from the Qt servers:: - wget https://download.qt.io/development_releases/prebuilt/libclang/libclang-release_18.1.5-based-macos-universal.7z + wget https://download.qt.io/development_releases/prebuilt/libclang/libclang-release_20.1.3-based-macos-universal.7z Extract the files, and leave it in any desired path, and set the environment variable required:: - 7z x libclang-release_18.1.5-based-macos-universal.7z + 7z x libclang-release_20.1.3-based-macos-universal.7z export LLVM_INSTALL_DIR=$PWD/libclang Getting PySide diff --git a/sources/pyside6/doc/building_from_source/windows.rst b/sources/pyside6/doc/building_from_source/windows.rst index 737d045b..0732953d 100644 --- a/sources/pyside6/doc/building_from_source/windows.rst +++ b/sources/pyside6/doc/building_from_source/windows.rst @@ -55,7 +55,10 @@ Setting up CLANG libclang can be downloaded from the `Qt servers `_. -for example, ``libclang-release_18.1.5-based-windows-vs2019_64.7z``. +for example, ``libclang-release_20.1.3-based-windows-vs2019_64.7z``. + +For ARM64 Windows, use: +``libclang-release_20.1.3-based-windows-vs2022_arm64.7z``. Note that from version 12 onwards, the prebuilt Windows binaries from `LLVM `_ no longer contain CMake configuration files; so diff --git a/sources/pyside6/doc/conf.py.in b/sources/pyside6/doc/conf.py.in index 5af91019..38c2c606 100644 --- a/sources/pyside6/doc/conf.py.in +++ b/sources/pyside6/doc/conf.py.in @@ -33,7 +33,7 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.ifconfig', 'sphinx.ext.graphviz', 'inheritance_diagram', 'pysideinclude', 'sphinx.ext.viewcode', 'sphinx_design', 'sphinx_copybutton', 'myst_parser', 'sphinx_tags', - 'sphinx_toolbox.decorators', 'sphinx_reredirects'] + 'sphinx_toolbox.collapse', 'sphinx_toolbox.decorators', 'sphinx_reredirects'] myst_enable_extensions = [ "amsmath", diff --git a/sources/pyside6/doc/deployment/deployment-pyside6-android-deploy.rst b/sources/pyside6/doc/deployment/deployment-pyside6-android-deploy.rst index 72e2b9cf..7d2064c7 100644 --- a/sources/pyside6/doc/deployment/deployment-pyside6-android-deploy.rst +++ b/sources/pyside6/doc/deployment/deployment-pyside6-android-deploy.rst @@ -16,7 +16,7 @@ The final output is a `.apk` or a `.aab` file created within the project's sourc determines whether a `.apk` or a `.aab` is created. .. warning:: Currently, users are required to cross-compile Qt for Python to generate the wheels - required for `armeabi-v7a` and `x86` Andorid platforms. Instructions on cross-compiling + required for `armeabi-v7a` and `x86` Android platforms. Instructions on cross-compiling Qt for Python for Android can be found :ref:`here `. .. note:: ``pyside6-android-deploy`` only works with a Unix (Linux or macOS) host at the moment. diff --git a/sources/pyside6/doc/developer/add_port_example.rst b/sources/pyside6/doc/developer/add_port_example.rst index 59aa2f11..e65c3c9c 100644 --- a/sources/pyside6/doc/developer/add_port_example.rst +++ b/sources/pyside6/doc/developer/add_port_example.rst @@ -27,7 +27,13 @@ For example: $ flake8 --config pyside-setup/.flake8 your_file.py $ isort your_file.py +Later on, the tool `tools/sync_examples.py` can be used to update the source +files from Qt C++. Each difference produced by tool needs to checked with care, +differences in `qmldir` files (`prefer` directive) might apply to C++ only. +Also, be careful not to back-port old Qt Widgets Designer `.ui` files with +unqualified enumerations. If a `.ui` file has additions, make sure to load and +save it at least once. Add a new example ----------------- @@ -61,11 +67,17 @@ Port a Qt example - Note that our examples need to have unique names due to the doc build. - Verify that all slots are decorated using ``@Slot``. - Enumerations should be fully qualified (PYSIDE-1735). +- Check the above by running the example with the environment variables: + + .. code-block:: bash + + export PYSIDE6_OPTION_PYTHON_ENUM=0x71 + export QT_LOGGING_RULES=qt.pyside.libpyside.warning=true + - Add a ``.pyproject`` file (verify later on that docs build). -- Add a ``doc`` directory and descriptive ``.rst`` file, +- Add a ``doc`` directory and descriptive ``.md`` or ``.rst`` file, and a screenshot if suitable (use ``optipng`` to reduce file size). - Add the ``"""Port of the ... example from Qt 6"""`` doc string. -- Try to port variable and function names to snake case convention. - Remove C++ documentation from ``sources/pyside6/doc/additionaldocs.lst``. .. note:: Example screenshots in ``.png`` should be optimized by diff --git a/sources/pyside6/doc/developer/documentation.rst b/sources/pyside6/doc/developer/documentation.rst index 53e91291..24c8d7a1 100644 --- a/sources/pyside6/doc/developer/documentation.rst +++ b/sources/pyside6/doc/developer/documentation.rst @@ -44,24 +44,30 @@ shiboken/sphinx. A line in brackets denotes the output directory. -The list can be created by the below script and some hand-editing. It will find -almost all documents. Quite a number of them might be unreferenced, but there -is no good way of filtering for this. -Pages of examples that exist in Python should be removed. +The list can be created by running the below script in the PySide6 build +directory and some hand-editing. It will find almost all documents. Quite +a number of them might be unreferenced, but there is no good way of filtering +for this. Pages of examples that exist in Python should be removed. .. code-block:: bash - for F in *.webxml - do - echo "$F" | egrep '(-index|example|cmake|private-module|-changes-qt6|-module.web|-qmlmodule.web)' > /dev/null - if [ $? -ne 0 ] - then - if fgrep '' "$F" > /dev/null # Exclude reference only + find_docs() + { + for F in $(find . -name "*.webxml") + do + echo "$F" | egrep '(-index|example|cmake|private-module|-changes-qt6|-module.web|-qmlmodule.web)' > /dev/null + if [ $? -ne 0 ] then - egrep "( /dev/null || echo $F + if fgrep '' "$F" > /dev/null # Exclude reference only + then + egrep "( /dev/null || echo $F + fi fi - fi - done + done + } + + cd doc/qdoc-output + find_docs | cut -c3- | sort The overviews go into a directory named ``overviews``. There are also special pages containing lists of classes with brief, grouped by function. They mostly diff --git a/sources/pyside6/doc/developer/extras.rst b/sources/pyside6/doc/developer/extras.rst index 1cccaad3..5d31d053 100644 --- a/sources/pyside6/doc/developer/extras.rst +++ b/sources/pyside6/doc/developer/extras.rst @@ -40,6 +40,40 @@ pre-loaded. Assuming the library is found at Lately, this feature has been added to MVSC, too. +Build with thread sanitizer +=========================== + +`Thread sanitizer `_ +can be useful for detecting data races, etc, for example when experimenting +with free threaded Python. It is similar to address sanitizer. + +For the build, the options ``--sanitize-thread`` and ``--disable-pyi`` should +be passed to prevent it terminating due to false positives when generating the +``.pyi`` files: + +.. code-block:: bash + + python setup.py build [...] --sanitize-thread --disable-pyi + +Similar to address sanitizer, a library needs to be pre-loaded +when running code: + +.. code-block:: bash + + export LD_PRELOAD=/usr/lib/gcc/x86_64-linux-gnu/13/libtsan.so + +.. note:: Thread sanitizer maybe report false positives (data races + for code that is protected by a ``QRecursiveMutex`` or a + ``std::recursive_mutex``). + +.. note:: When the error `Unexpected memory mapping` occurs, it helps to execute: + + .. code-block:: bash + + sudo sysctl vm.mmap_rnd_bits=28 + + See `Article on stackoverflow `_\. + De-Virtualize the Python Files ============================== diff --git a/sources/pyside6/doc/developer/index.rst b/sources/pyside6/doc/developer/index.rst index 296b455b..88e3ab30 100644 --- a/sources/pyside6/doc/developer/index.rst +++ b/sources/pyside6/doc/developer/index.rst @@ -16,6 +16,7 @@ Development Topics add_module.rst add_port_example.rst add_tool.rst + pythonversions.md documentation.rst adapt_qt.rst extras.rst diff --git a/sources/pyside6/doc/developer/pythonversions.md b/sources/pyside6/doc/developer/pythonversions.md new file mode 100644 index 00000000..baef18d7 --- /dev/null +++ b/sources/pyside6/doc/developer/pythonversions.md @@ -0,0 +1,63 @@ +# Adapting to changes in supported Python versions + +## Relevant preprocessor defines + +- The version range is determined by `wheel_artifacts/pyproject.toml.base`. + This file also defines the version tag (`py_limited_api = "cp310"`). +- `PY_VERSION_HEX` Python version (defined in CPython headers) +- `Py_LIMITED_API` Limited API minimum version, defined in several CMake files +- `PYPY_VERSION` [PyPy](https://pypy.org/) version (defined in PyPy headers) + +## Removing outdated Python versions + +The removal of Python versions is tied to their lifetime +(see [Wiki](https://wiki.qt.io/Qt_for_Python)). + +- Raise the `Py_LIMITED_API` definition in the CMake files. +- Check the source code for preprocessor defines depending on + values `Py_LIMITED_API`, `PY_VERSION_HEX` and simplify or + remove conditions if possible. +- Check the usages of `_PepRuntimeVersion()` for outdated versions +- Run the tests and some examples. There might actually + some version checks in Python code that trigger + (see for example + `sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/parser.py:70`). + +## Adapting to new Python versions + +New versions appear as branches in the `https://github.com/python/cpython.git` +repository, are developed over the course of a year and released around fall. +Change logs and information about deprecations are found in the directory +`Doc/whatsnew`. + +It is recommended to build a release and a debug version of it and check +whether PySide works with it from time to time. + +It is possible that some breakages occur that are fixed later in the +development process, so, one should not prematurely submit fixes to PySide. + +A debug version of CPython can be build from a shadow build directory +using: +``` +/configure --prefix= --enable-shared --with-ensurepip=install \ + -with-pydebug --with-trace-refs --with-valgrind \ + "CFLAGS=-O0 -g -fno-inline -fno-omit-frame-pointer" CPPFLAGS=-O0 LDFLAGS=-O0 +make && make install +``` + +For a release build: + +``` +/configure --prefix= --enable-shared --with-ensurepip=install \ + --enable-optimizations +make && make install +``` + +Those binaries can then be used to create `venv`s and build PySide normally. + +Tests should always pass in the release build. The debug build might +have some test failures; but it should not assert. + +It should also be checked whether PySide compiles when raising the Limited API +minimum version to the new version (although the change can only be submitted +much later). diff --git a/sources/pyside6/doc/extras/QtCore.Property.rst b/sources/pyside6/doc/extras/QtCore.Property.rst index da2ccc60..db72b61e 100644 --- a/sources/pyside6/doc/extras/QtCore.Property.rst +++ b/sources/pyside6/doc/extras/QtCore.Property.rst @@ -48,7 +48,7 @@ PySide6.QtCore.Property freset: Optional[Callable] = None, fdel: Optional[Callable] = None, doc: str = '', - notify: Optional[Callable] = None, + notify: Optional[PySide6.QtCore.Signal] = None, designable: bool = True, scriptable: bool = True, stored: bool = True, user: bool = False, diff --git a/sources/pyside6/doc/extras/QtQuick.rst b/sources/pyside6/doc/extras/QtQuick.rst index 1027c1fe..1aa38500 100644 --- a/sources/pyside6/doc/extras/QtQuick.rst +++ b/sources/pyside6/doc/extras/QtQuick.rst @@ -42,7 +42,7 @@ shader effects. * :ref:`Important-Concepts-In-Qt-Quick---User-Input` * :ref:`Important-Concepts-In-Qt-Quick---Positioning` * :ref:`Important-Concepts-in-Qt-Quick---States--Transitions-and-Animations` - * :ref:`Important-Concepts-In-Qt-Quick---Data---Models--Views-and-Data-Storage` + * :ref:`Important-Concepts-In-Qt-Quick---Data---Models--Views--and-Data-Storage` * :ref:`Important-Concepts-In-Qt-Quick---Graphical-Effects` * `MultiEffect `_ * :ref:`Important-Concepts-In-Qt-Quick---Convenience-Types` @@ -66,7 +66,7 @@ Articles and Guides Further information for writing QML applications: - * :ref:`QML-Applications` - Essential information for application development with QML and Qt Quick + * :ref:`Getting started with Qt Quick applications ` - Essential information for application development with QML and Qt Quick * :mod:`Qt Qml ` - Documentation for the Qt QML module, which provides the QML engine and language infrastructure * :ref:`Qt Quick How-tos` - shows how to achieve specific tasks in Qt Quick diff --git a/sources/pyside6/doc/gettingstarted.rst b/sources/pyside6/doc/gettingstarted.rst index 9abe6456..9f4b97f8 100644 --- a/sources/pyside6/doc/gettingstarted.rst +++ b/sources/pyside6/doc/gettingstarted.rst @@ -282,6 +282,10 @@ Next steps Now that you have use both technologies, you can head to our :ref:`pyside6_examples` and :ref:`pyside6_tutorials` sections. +.. tip:: **Visual Studio Code Users**: If you use VSCode, check out the + :ref:`vscode-ext` which provides project templates, debugging support, + build tasks, and other productivity features for PySide6 development. + .. _faq-section: Frequently Asked Questions diff --git a/sources/pyside6/doc/tools/index.rst b/sources/pyside6/doc/tools/index.rst index 0f6cd03f..6e3057b3 100644 --- a/sources/pyside6/doc/tools/index.rst +++ b/sources/pyside6/doc/tools/index.rst @@ -163,6 +163,19 @@ PySide Utilities a tool to print out the metatype information in JSON to be used as input for ``qmltyperegistrar``. +IDE Integration +~~~~~~~~~~~~~~~ + +.. grid:: 2 + :gutter: 3 3 4 5 + + .. grid-item-card:: Qt Python VSCode Extension + :link: vscode-ext + :link-type: ref + + Visual Studio Code extension for PySide6 development with project + templates, debugging, build tasks, and more. + Deployment ~~~~~~~~~~ @@ -237,3 +250,4 @@ Qt Quick 3D pyside6-balsamui pyside6-qmlimportscanner pyside6-qsb + vscode-ext diff --git a/sources/pyside6/doc/tools/vscode-ext.rst b/sources/pyside6/doc/tools/vscode-ext.rst new file mode 100644 index 00000000..a0f67736 --- /dev/null +++ b/sources/pyside6/doc/tools/vscode-ext.rst @@ -0,0 +1,73 @@ +.. _vscode-ext: + +Qt Python VSCode Extension +************************** + +The `Qt Python extension`_ for Visual Studio Code is a comprehensive development tool +that enhances your PySide6 development workflow with integrated debugging, project +templates, and build tasks. + +Installation +============ + +Install the extension from the Visual Studio Code Marketplace: + +1. Open VSCode +2. Go to the Extensions view +3. Search for "Qt Python" +4. Click Install on the extension published by **The Qt Company** + +Alternatively, install from the command line: + +.. code-block:: bash + + code --install-extension TheQtCompany.qt-python + +Features +======== + +Project Creation +---------------- + +Create new PySide6 projects using templates: + +1. Open the Command Palette +2. Type and select **Qt: Create a new Project or file** +3. Choose from available templates: + + * **Python QtQuick Application** - Creates a Qt Quick/QML project structure + * **Python QtWidgets Application** - Creates a Qt Widgets project structure + +PySide6 Installation +-------------------- + +Quickly install PySide6 in your current Python environment: + +1. Open the Command Palette +2. Type and select **Qt-Python: Install PySide6** + +Build Tasks +----------- + +The extension provides PySide6-specific tasks accessible via **Tasks: Run Task**: + +* **PySide: build** - Build your PySide6 project (compiles UI files, resources, etc.) +* **PySide: run** - Run your PySide6 application +* **PySide: clean** - Clean build artifacts +* **PySide: deploy** - Deploy your application using pyside6-deploy + +Debugging +--------- + +The extension provides debugging capabilities for PySide6 applications with support +for both Python and QML code. See :ref:`tutorial_qml_debugging` for detailed information +on debugging Qt Quick applications with mixed Python/QML debugging. + +Learn More +========== + +For detailed documentation, feature updates, and usage instructions, visit the +`Qt Python extension marketplace page`_. + +.. _`Qt Python extension`: https://marketplace.visualstudio.com/items?itemName=TheQtCompany.qt-python +.. _`Qt Python extension marketplace page`: https://marketplace.visualstudio.com/items?itemName=TheQtCompany.qt-python diff --git a/sources/pyside6/doc/tutorials/basictutorial/qrcfiles.rst b/sources/pyside6/doc/tutorials/basictutorial/qrcfiles.rst index b6861f92..2666f854 100644 --- a/sources/pyside6/doc/tutorials/basictutorial/qrcfiles.rst +++ b/sources/pyside6/doc/tutorials/basictutorial/qrcfiles.rst @@ -75,6 +75,11 @@ To use the generated file, add the following import at the top of your main Pyth import rc_icons +.. note:: The tool uses `Zstandard` as a compression algorithm, which at its default + compression level (implementation-defined) may produce results that are + not usable on other platforms. To ensure the files are usable on all + platforms, the compression level should be reduced or `zlib` should + be chosen as compression algorithm (see ``pyside6-rcc --help``). Changes in the code =================== diff --git a/sources/pyside6/doc/tutorials/basictutorial/uifiles.rst b/sources/pyside6/doc/tutorials/basictutorial/uifiles.rst index 32d32300..6d17f039 100644 --- a/sources/pyside6/doc/tutorials/basictutorial/uifiles.rst +++ b/sources/pyside6/doc/tutorials/basictutorial/uifiles.rst @@ -283,7 +283,7 @@ the custom widget should be visible in the widget box. For advanced usage, it is also possible to pass the function an implementation of the class QDesignerCustomWidgetInterface instead of the type to :meth:`addCustomWidget()`. -This is shown in taskmenuextension example, where a custom context menu +This is shown in the :ref:`example_designer_taskmenuextension`, where a custom context menu is registered for the custom widget. The example is a port of the corresponding C++ `Task Menu Extension Example `_ . @@ -291,6 +291,40 @@ corresponding C++ .. _QDesignerCustomWidgetCollectionInterface: https://doc.qt.io/qt-6/qdesignercustomwidgetcollectioninterface.html .. _QDesignerCustomWidgetInterface: https://doc.qt.io/qt-6/qdesignercustomwidgetinterface.html +Writing Custom Widgets +++++++++++++++++++++++ + +For properties to become visible in *Qt Widgets Designer*, they need to be +declared using :class:`PySide6.QtCore.Property`. + +Enums and flag types need to appear within the class and be decorated using +:deco:`PySide6.QtCore.QEnum` or :deco:`PySide6.QtCore.QFlag`, respectively. +This requires extracting a base class for them since otherwise, the enum type +is not known when specifying :class:`PySide6.QtCore.Property`: + +.. code-block:: python + + class CustomWidgetBase(QObject): + @QEnum + class TestEnum(Enum): + EnumValue0 = 0 + EnumValue1 = 1 + + + class CustomWidget(CustomWidgetBase): + def __init__(self, parent=None): + super().__init__(parent) + self._testEnum = CustomWidget.TestEnum.EnumValue1 + + def testEnum(self): + return self._testEnum + + def setTestEnum(self, new_val): + self._testEnum = new_val + + testEnum = Property(CustomWidgetBase.TestEnum, testEnum, setTestEnum) + + Troubleshooting the Qt Widgets Designer Plugin ++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/sources/pyside6/doc/tutorials/datavisualize/add_chart.rst b/sources/pyside6/doc/tutorials/datavisualize/add_chart.rst index acffce40..87e01202 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/add_chart.rst +++ b/sources/pyside6/doc/tutorials/datavisualize/add_chart.rst @@ -5,17 +5,17 @@ Chapter 5 - Add a chart view ============================= A table is nice to present data, but a chart is even better. For this, you -need the QtCharts module that provides many types of plots and options to +need the QtGraphs module that provides many types of plots and options to graphically represent data. -The placeholder for a plot is a QChartView, and inside that Widget you can -place a QChart. As a first step, try including only this without any data to -plot. +The relevant class for a plot is the GraphsView QML type, to which axes and +data series can be added. As a first step, try including then without any data +to plot. Make the following highlighted changes to :code:`main_widget.py` from the -previous chapter to add a QChartView: +previous chapter to add a chart: .. literalinclude:: datavisualize5/main_widget.py :linenos: - :lines: 3- - :emphasize-lines: 2-3,6,22-36,47-49 + :lines: 5- + :emphasize-lines: 4,27-39,53 diff --git a/sources/pyside6/doc/tutorials/datavisualize/add_mainwindow.rst b/sources/pyside6/doc/tutorials/datavisualize/add_mainwindow.rst index ab5468cd..9073e679 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/add_mainwindow.rst +++ b/sources/pyside6/doc/tutorials/datavisualize/add_mainwindow.rst @@ -4,7 +4,8 @@ Chapter 3 - Create an empty QMainWindow ========================================== -You can now think of presenting your data in a UI. A QMainWindow provides a +You can now think of presenting your data in a UI. A +class:`~PySide6.QtWidgets.QMainWindow` provides a convenient structure for GUI applications, such as a menu bar and status bar. The following image shows the layout that QMainWindow offers out-of-the box: @@ -24,12 +25,13 @@ the resolution you currently have. In the following snippet, you will see how window size is defined based on available screen width (80%) and height (70%). .. note:: You can achieve a similar structure using other Qt elements like - QMenuBar, QWidget, and QStatusBar. Refer the QMainWindow layout for + class:`~PySide6.QtWidgets.QMenuBar`, class:`~PySide6.QtWidgets.QWidget`, + and class:`~PySide6.QtWidgets.QStatusBar`. Refer the QMainWindow layout for guidance. .. literalinclude:: datavisualize3/main_window.py :language: python :linenos: - :lines: 4- + :lines: 5- Try running the script to see what output you get with it. diff --git a/sources/pyside6/doc/tutorials/datavisualize/add_tableview.rst b/sources/pyside6/doc/tutorials/datavisualize/add_tableview.rst index 5b7e5e73..b3041349 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/add_tableview.rst +++ b/sources/pyside6/doc/tutorials/datavisualize/add_tableview.rst @@ -8,21 +8,22 @@ Now that you have a QMainWindow, you can include a centralWidget to your interface. Usually, a QWidget is used to display data in most data-driven applications. Use a table view to display your data. -The first step is to add a horizontal layout with just a QTableView. You -can create a QTableView object and place it inside a QHBoxLayout. Once the +The first step is to add a horizontal layout with just a +class:`~PySide6.QtWidgets.QTableView`. You can create a QTableView object +and place it inside a class:`~PySide6.QtWidgets.QHBoxLayout`. Once the QWidget is properly built, pass the object to the QMainWindow as its central widget. Remember that a QTableView needs a model to display information. In this case, -you can use a QAbstractTableModel instance. +you can use a class:`~PySide6.QtCore.QAbstractTableModel` instance. .. note:: You could also use the default item model that comes with a - QTableWidget instead. QTableWidget is a convenience class that reduces - your codebase considerably as you don't need to implement a data model. - However, it's less flexible than a QTableView, as QTableWidget cannot be - used with just any data. For more insight about Qt's model-view framework, - refer to the - `Model View Programming ` + class:`~PySide6.QtWidgets.QTableWidget` instead. QTableWidget is a + convenience class that reduces your codebase considerably as you don't need + to implement a data model. However, it's less flexible than a QTableView, + as QTableWidget cannot be used with just any data. For more insight about + Qt's model-view framework, refer to the + `Model View Programming ` documentation. Implementing the model for your QTableView, allows you to: @@ -42,7 +43,7 @@ Here is a script that implements the CustomTableModel: .. literalinclude:: datavisualize4/table_model.py :language: python :linenos: - :lines: 3- + :lines: 5- Now, create a QWidget that has a QTableView, and connect it to your CustomTableModel. @@ -50,8 +51,8 @@ CustomTableModel. .. literalinclude:: datavisualize4/main_widget.py :language: python :linenos: - :emphasize-lines: 12-17 - :lines: 3- + :emphasize-lines: 12-12 + :lines: 5- You also need minor changes to the :code:`main_window.py` and :code:`main.py` from chapter 3 to include the Widget inside the @@ -62,11 +63,11 @@ In the following snippets you'll see those changes highlighted: .. literalinclude:: datavisualize4/main_window.py :language: python :linenos: - :lines: 3- - :emphasize-lines: 8,11 + :lines: 5- + :emphasize-lines: 9 .. literalinclude:: datavisualize4/main.py :language: python :linenos: - :lines: 3- - :emphasize-lines: 46-47 + :lines: 5- + :emphasize-lines: 45-46 diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/datavisualize3.pyproject b/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/datavisualize3.pyproject new file mode 100644 index 00000000..1bd31f95 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/datavisualize3.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["main.py", "main_window.py"] +} diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/main_window.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/main_window.py index 6ee8fa61..79a4afd3 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/main_window.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/main_window.py @@ -2,25 +2,22 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause from __future__ import annotations -from PySide6.QtGui import QAction, QKeySequence +from PySide6.QtGui import QIcon, QKeySequence from PySide6.QtWidgets import QMainWindow class MainWindow(QMainWindow): def __init__(self): - QMainWindow.__init__(self) + super().__init__() self.setWindowTitle("Eartquakes information") # Menu self.menu = self.menuBar() - self.file_menu = self.menu.addMenu("File") + file_menu = self.menu.addMenu("File") # Exit QAction - exit_action = QAction("Exit", self) - exit_action.setShortcut(QKeySequence.Quit) - exit_action.triggered.connect(self.close) - - self.file_menu.addAction(exit_action) + file_menu.addAction(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit), + "Exit", QKeySequence.StandardKey.Quit, self.close) # Status Bar self.status = self.statusBar() diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/datavisualize4.pyproject b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/datavisualize4.pyproject new file mode 100644 index 00000000..f5496972 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/datavisualize4.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["main.py", "main_widget.py", "main_window.py", "table_model.py"] +} diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_widget.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_widget.py index 85e24833..5d8e6ade 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_widget.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_widget.py @@ -10,7 +10,7 @@ from table_model import CustomTableModel class Widget(QWidget): def __init__(self, data): - QWidget.__init__(self) + super().__init__() # Getting the Model self.model = CustomTableModel(data) @@ -22,13 +22,13 @@ class Widget(QWidget): # QTableView Headers self.horizontal_header = self.table_view.horizontalHeader() self.vertical_header = self.table_view.verticalHeader() - self.horizontal_header.setSectionResizeMode(QHeaderView.ResizeToContents) - self.vertical_header.setSectionResizeMode(QHeaderView.ResizeToContents) + self.horizontal_header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + self.vertical_header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) self.horizontal_header.setStretchLastSection(True) # QWidget Layout self.main_layout = QHBoxLayout() - size = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + size = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) # Left layout size.setHorizontalStretch(1) diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_window.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_window.py index ded7fdf5..600af650 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_window.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_window.py @@ -2,25 +2,22 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause from __future__ import annotations -from PySide6.QtGui import QAction, QKeySequence +from PySide6.QtGui import QIcon, QKeySequence from PySide6.QtWidgets import QMainWindow class MainWindow(QMainWindow): def __init__(self, widget): - QMainWindow.__init__(self) + super().__init__() self.setWindowTitle("Eartquakes information") self.setCentralWidget(widget) # Menu self.menu = self.menuBar() - self.file_menu = self.menu.addMenu("File") + file_menu = self.menu.addMenu("File") # Exit QAction - exit_action = QAction("Exit", self) - exit_action.setShortcut(QKeySequence.Quit) - exit_action.triggered.connect(self.close) - - self.file_menu.addAction(exit_action) + file_menu.addAction(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit), + "Exit", QKeySequence.StandardKey.Quit, self.close) # Status Bar self.status = self.statusBar() diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/table_model.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/table_model.py index cc2ac12a..9a2871c2 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/table_model.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/table_model.py @@ -25,27 +25,27 @@ class CustomTableModel(QAbstractTableModel): return self.column_count def headerData(self, section, orientation, role): - if role != Qt.DisplayRole: + if role != Qt.ItemDataRole.DisplayRole: return None - if orientation == Qt.Horizontal: + if orientation == Qt.Orientation.Horizontal: return ("Date", "Magnitude")[section] else: return f"{section}" - def data(self, index, role=Qt.DisplayRole): + def data(self, index, role=Qt.ItemDataRole.DisplayRole): column = index.column() row = index.row() - if role == Qt.DisplayRole: + if role == Qt.ItemDataRole.DisplayRole: if column == 0: date = self.input_dates[row].toPython() return str(date)[:-3] elif column == 1: magnitude = self.input_magnitudes[row] return f"{magnitude:.2f}" - elif role == Qt.BackgroundRole: - return QColor(Qt.white) - elif role == Qt.TextAlignmentRole: - return Qt.AlignRight + elif role == Qt.ItemDataRole.BackgroundRole: + return QColor(Qt.GlobalColor.white) + elif role == Qt.ItemDataRole.TextAlignmentRole: + return Qt.AlignmentFlag.AlignRight return None diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/datavisualize5.pyproject b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/datavisualize5.pyproject new file mode 100644 index 00000000..f5496972 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/datavisualize5.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["main.py", "main_widget.py", "main_window.py", "table_model.py"] +} diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_widget.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_widget.py index 77ea4e77..fca09b05 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_widget.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_widget.py @@ -2,17 +2,17 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause from __future__ import annotations -from PySide6.QtGui import QPainter from PySide6.QtWidgets import (QWidget, QHeaderView, QHBoxLayout, QTableView, QSizePolicy) -from PySide6.QtCharts import QChart, QChartView +from PySide6.QtQuickWidgets import QQuickWidget +from PySide6.QtGraphs import QLineSeries, QDateTimeAxis, QValueAxis, QGraphsTheme from table_model import CustomTableModel class Widget(QWidget): def __init__(self, data): - QWidget.__init__(self) + super().__init__() # Getting the Model self.model = CustomTableModel(data) @@ -24,21 +24,27 @@ class Widget(QWidget): # QTableView Headers self.horizontal_header = self.table_view.horizontalHeader() self.vertical_header = self.table_view.verticalHeader() - self.horizontal_header.setSectionResizeMode(QHeaderView.ResizeToContents) - self.vertical_header.setSectionResizeMode(QHeaderView.ResizeToContents) + self.horizontal_header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + self.vertical_header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) self.horizontal_header.setStretchLastSection(True) - # Creating QChart - self.chart = QChart() - self.chart.setAnimationOptions(QChart.AllAnimations) - - # Creating QChartView - self.chart_view = QChartView(self.chart) - self.chart_view.setRenderHint(QPainter.Antialiasing) + # Create QGraphView via QML + self.series = QLineSeries() + self.axis_x = QDateTimeAxis() + self.axis_y = QValueAxis() + self.quick_widget = QQuickWidget(self) + self.quick_widget.setResizeMode(QQuickWidget.ResizeMode.SizeRootObjectToView) + self.theme = QGraphsTheme() + initial_properties = {"theme": self.theme, + "axisX": self.axis_x, + "axisY": self.axis_y, + "seriesList": self.series} + self.quick_widget.setInitialProperties(initial_properties) + self.quick_widget.loadFromModule("QtGraphs", "GraphsView") # QWidget Layout - self.main_layout = QHBoxLayout() - size = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + self.main_layout = QHBoxLayout(self) + size = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) # Left layout size.setHorizontalStretch(1) @@ -47,8 +53,5 @@ class Widget(QWidget): # Right Layout size.setHorizontalStretch(4) - self.chart_view.setSizePolicy(size) - self.main_layout.addWidget(self.chart_view) - - # Set the layout to the QWidget - self.setLayout(self.main_layout) + self.quick_widget.setSizePolicy(size) + self.main_layout.addWidget(self.quick_widget) diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_window.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_window.py index ded7fdf5..600af650 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_window.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_window.py @@ -2,25 +2,22 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause from __future__ import annotations -from PySide6.QtGui import QAction, QKeySequence +from PySide6.QtGui import QIcon, QKeySequence from PySide6.QtWidgets import QMainWindow class MainWindow(QMainWindow): def __init__(self, widget): - QMainWindow.__init__(self) + super().__init__() self.setWindowTitle("Eartquakes information") self.setCentralWidget(widget) # Menu self.menu = self.menuBar() - self.file_menu = self.menu.addMenu("File") + file_menu = self.menu.addMenu("File") # Exit QAction - exit_action = QAction("Exit", self) - exit_action.setShortcut(QKeySequence.Quit) - exit_action.triggered.connect(self.close) - - self.file_menu.addAction(exit_action) + file_menu.addAction(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit), + "Exit", QKeySequence.StandardKey.Quit, self.close) # Status Bar self.status = self.statusBar() diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/table_model.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/table_model.py index cc2ac12a..9a2871c2 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/table_model.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/table_model.py @@ -25,27 +25,27 @@ class CustomTableModel(QAbstractTableModel): return self.column_count def headerData(self, section, orientation, role): - if role != Qt.DisplayRole: + if role != Qt.ItemDataRole.DisplayRole: return None - if orientation == Qt.Horizontal: + if orientation == Qt.Orientation.Horizontal: return ("Date", "Magnitude")[section] else: return f"{section}" - def data(self, index, role=Qt.DisplayRole): + def data(self, index, role=Qt.ItemDataRole.DisplayRole): column = index.column() row = index.row() - if role == Qt.DisplayRole: + if role == Qt.ItemDataRole.DisplayRole: if column == 0: date = self.input_dates[row].toPython() return str(date)[:-3] elif column == 1: magnitude = self.input_magnitudes[row] return f"{magnitude:.2f}" - elif role == Qt.BackgroundRole: - return QColor(Qt.white) - elif role == Qt.TextAlignmentRole: - return Qt.AlignRight + elif role == Qt.ItemDataRole.BackgroundRole: + return QColor(Qt.GlobalColor.white) + elif role == Qt.ItemDataRole.TextAlignmentRole: + return Qt.AlignmentFlag.AlignRight return None diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/datavisualize6.pyproject b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/datavisualize6.pyproject new file mode 100644 index 00000000..f5496972 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/datavisualize6.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["main.py", "main_widget.py", "main_window.py", "table_model.py"] +} diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_widget.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_widget.py index f987689e..336afacd 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_widget.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_widget.py @@ -2,18 +2,20 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause from __future__ import annotations -from PySide6.QtCore import QDateTime, Qt -from PySide6.QtGui import QPainter +from math import floor, ceil + +from PySide6.QtCore import QDateTime, QTime, QTimeZone from PySide6.QtWidgets import (QWidget, QHeaderView, QHBoxLayout, QTableView, QSizePolicy) -from PySide6.QtCharts import QChart, QChartView, QLineSeries, QDateTimeAxis, QValueAxis +from PySide6.QtQuickWidgets import QQuickWidget +from PySide6.QtGraphs import QLineSeries, QDateTimeAxis, QValueAxis, QGraphsTheme from table_model import CustomTableModel class Widget(QWidget): def __init__(self, data): - QWidget.__init__(self) + super().__init__() # Getting the Model self.model = CustomTableModel(data) @@ -23,25 +25,29 @@ class Widget(QWidget): self.table_view.setModel(self.model) # QTableView Headers - resize = QHeaderView.ResizeToContents + resize = QHeaderView.ResizeMode.ResizeToContents self.horizontal_header = self.table_view.horizontalHeader() self.vertical_header = self.table_view.verticalHeader() self.horizontal_header.setSectionResizeMode(resize) self.vertical_header.setSectionResizeMode(resize) self.horizontal_header.setStretchLastSection(True) - # Creating QChart - self.chart = QChart() - self.chart.setAnimationOptions(QChart.AllAnimations) - self.add_series("Magnitude (Column 1)", [0, 1]) - - # Creating QChartView - self.chart_view = QChartView(self.chart) - self.chart_view.setRenderHint(QPainter.Antialiasing) + # Create QGraphView via QML + self.populate_series() + self.quick_widget = QQuickWidget(self) + self.quick_widget.setResizeMode(QQuickWidget.ResizeMode.SizeRootObjectToView) + self.theme = QGraphsTheme() + self.theme.setTheme(QGraphsTheme.Theme.BlueSeries) + initial_properties = {"theme": self.theme, + "axisX": self.axis_x, + "axisY": self.axis_y, + "seriesList": self.series} + self.quick_widget.setInitialProperties(initial_properties) + self.quick_widget.loadFromModule("QtGraphs", "GraphsView") # QWidget Layout - self.main_layout = QHBoxLayout() - size = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + self.main_layout = QHBoxLayout(self) + size = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) # Left layout size.setHorizontalStretch(1) @@ -50,46 +56,51 @@ class Widget(QWidget): # Right Layout size.setHorizontalStretch(4) - self.chart_view.setSizePolicy(size) - self.main_layout.addWidget(self.chart_view) + self.quick_widget.setSizePolicy(size) + self.main_layout.addWidget(self.quick_widget) - # Set the layout to the QWidget - self.setLayout(self.main_layout) + def populate_series(self): + def seconds(qtime: QTime): + return qtime.minute() * 60 + qtime.second() - def add_series(self, name, columns): - # Create QLineSeries self.series = QLineSeries() - self.series.setName(name) + self.series.setName("Magnitude (Column 1)") # Filling QLineSeries + time_min = QDateTime(2100, 1, 1, 0, 0, 0) + time_max = QDateTime(1970, 1, 1, 0, 0, 0) + time_zone = QTimeZone(QTimeZone.Initialization.UTC) + y_min = 1e37 + y_max = -1e37 + date_fmt = "yyyy-MM-dd HH:mm:ss.zzz" for i in range(self.model.rowCount()): - # Getting the data t = self.model.index(i, 0).data() - date_fmt = "yyyy-MM-dd HH:mm:ss.zzz" - - x = QDateTime().fromString(t, date_fmt).toSecsSinceEpoch() + time = QDateTime.fromString(t, date_fmt) + time.setTimeZone(time_zone) y = float(self.model.index(i, 1).data()) - - if x > 0 and y > 0: - self.series.append(x, y) - - self.chart.addSeries(self.series) + if time.isValid() and y > 0: + if time > time_max: + time_max = time + if time < time_min: + time_min = time + if y > y_max: + y_max = y + if y < y_min: + y_min = y + self.series.append(time.toMSecsSinceEpoch(), y) # Setting X-axis self.axis_x = QDateTimeAxis() - self.axis_x.setTickCount(10) - self.axis_x.setFormat("dd.MM (h:mm)") + self.axis_x.setLabelFormat("dd.MM (h:mm)") self.axis_x.setTitleText("Date") - self.chart.addAxis(self.axis_x, Qt.AlignBottom) - self.series.attachAxis(self.axis_x) + self.axis_x.setMin(time_min.addSecs(-seconds(time_min.time()))) + self.axis_x.setMax(time_max.addSecs(3600 - seconds(time_max.time()))) + self.series.setAxisX(self.axis_x) + # Setting Y-axis self.axis_y = QValueAxis() - self.axis_y.setTickCount(10) self.axis_y.setLabelFormat("%.2f") self.axis_y.setTitleText("Magnitude") - self.chart.addAxis(self.axis_y, Qt.AlignLeft) - self.series.attachAxis(self.axis_y) - - # Getting the color from the QChart to use it on the QTableView - color_name = self.series.pen().color().name() - self.model.color = f"{color_name}" + self.axis_y.setMin(floor(y_min)) + self.axis_y.setMax(ceil(y_max)) + self.series.setAxisY(self.axis_y) diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_window.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_window.py index f37268df..6a9eaea8 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_window.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_window.py @@ -2,26 +2,22 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause from __future__ import annotations -from PySide6.QtGui import QAction, QKeySequence +from PySide6.QtGui import QIcon, QKeySequence from PySide6.QtWidgets import QMainWindow class MainWindow(QMainWindow): def __init__(self, widget): - QMainWindow.__init__(self) + super().__init__() self.setWindowTitle("Eartquakes information") # Menu self.menu = self.menuBar() - self.file_menu = self.menu.addMenu("File") + file_menu = self.menu.addMenu("File") # Exit QAction - exit_action = QAction("Exit", self) - exit_action.setShortcut(QKeySequence.Quit) - exit_action.triggered.connect(self.close) - - self.file_menu.addAction(exit_action) - + file_menu.addAction(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit), + "Exit", QKeySequence.StandardKey.Quit, self.close) # Status Bar self.status = self.statusBar() self.status.showMessage("Data loaded and plotted") diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/table_model.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/table_model.py index 3201e588..9a2871c2 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/table_model.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/table_model.py @@ -9,7 +9,6 @@ from PySide6.QtGui import QColor class CustomTableModel(QAbstractTableModel): def __init__(self, data=None): QAbstractTableModel.__init__(self) - self.color = None self.load_data(data) def load_data(self, data): @@ -26,27 +25,27 @@ class CustomTableModel(QAbstractTableModel): return self.column_count def headerData(self, section, orientation, role): - if role != Qt.DisplayRole: + if role != Qt.ItemDataRole.DisplayRole: return None - if orientation == Qt.Horizontal: + if orientation == Qt.Orientation.Horizontal: return ("Date", "Magnitude")[section] else: return f"{section}" - def data(self, index, role=Qt.DisplayRole): + def data(self, index, role=Qt.ItemDataRole.DisplayRole): column = index.column() row = index.row() - if role == Qt.DisplayRole: + if role == Qt.ItemDataRole.DisplayRole: if column == 0: date = self.input_dates[row].toPython() return str(date)[:-3] elif column == 1: magnitude = self.input_magnitudes[row] return f"{magnitude:.2f}" - elif role == Qt.BackgroundRole: - return (QColor(Qt.white), QColor(self.color))[column] - elif role == Qt.TextAlignmentRole: - return Qt.AlignRight + elif role == Qt.ItemDataRole.BackgroundRole: + return QColor(Qt.GlobalColor.white) + elif role == Qt.ItemDataRole.TextAlignmentRole: + return Qt.AlignmentFlag.AlignRight return None diff --git a/sources/pyside6/doc/tutorials/datavisualize/filter_data.rst b/sources/pyside6/doc/tutorials/datavisualize/filter_data.rst index bef134e5..4edde69c 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/filter_data.rst +++ b/sources/pyside6/doc/tutorials/datavisualize/filter_data.rst @@ -17,7 +17,8 @@ be done by filtering the data that follows the condition, "magnitude > 0", to avoid faulty data or unexpected behavior. The Date column provides data in UTC format (for example, -2018-12-11T21:14:44.682Z), so you could easily map it to a QDateTime object +2018-12-11T21:14:44.682Z), so you could easily map it to a +class:`~PySide6.QtCore.QDateTime` object defining the structure of the string. Additionally, you can adapt the time based on the timezone you are in, using QTimeZone. @@ -26,7 +27,7 @@ The following script filters and formats the CSV data as described earlier: .. literalinclude:: datavisualize2/main.py :language: python :linenos: - :lines: 3- + :lines: 5- -Now that you have a tuple of QDateTime and float data, try improving the +Now that you have a tuple of ``QDateTime`` and float data, try improving the output further. That's what you'll learn in the following chapters. diff --git a/sources/pyside6/doc/tutorials/datavisualize/plot_datapoints.rst b/sources/pyside6/doc/tutorials/datavisualize/plot_datapoints.rst index e4374e86..47d12a7c 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/plot_datapoints.rst +++ b/sources/pyside6/doc/tutorials/datavisualize/plot_datapoints.rst @@ -1,23 +1,23 @@ .. _tutorial_plot_datapoints: -Chapter 6 - Plot the data in the ChartView +Chapter 6 - Plot the data in the GraphsView =========================================== -The last step of this tutorial is to plot the CSV data inside our QChart. For -this, you need to go over our data and include the data on a QLineSeries. +The last step of this tutorial is to plot the CSV data inside our GraphsView. +For this, you need to go over our data and include the data on a QLineSeries. After adding the data to the series, you can modify the axis to properly display the QDateTime on the X-axis, and the magnitude values on the Y-axis. Here is the updated :code:`main_widget.py` that includes an additional -function to plot data using a QLineSeries: +function to plot data using a :class:`~PySide6.QtGraphs.QLineSeries`: .. literalinclude:: datavisualize6/main_widget.py :language: python :linenos: - :lines: 3- - :emphasize-lines: 33,56-91 + :lines: 5- + :emphasize-lines: 31-42, 68-102 Now, run the application to visualize the earthquake magnitudes data at different times. diff --git a/sources/pyside6/doc/tutorials/datavisualize/read_data.rst b/sources/pyside6/doc/tutorials/datavisualize/read_data.rst index 8be0e1c2..d083a05e 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/read_data.rst +++ b/sources/pyside6/doc/tutorials/datavisualize/read_data.rst @@ -21,7 +21,7 @@ The following python script, :code:`main.py`, demonstrates how to do it: .. literalinclude:: datavisualize1/main.py :language: python :linenos: - :lines: 3- + :lines: 5- The Python script uses the :code:`argparse` module to accept and parse input from the command line. It then uses the input, which in this case is the filename, diff --git a/sources/pyside6/doc/tutorials/debugging/qml_debugging.rst b/sources/pyside6/doc/tutorials/debugging/qml_debugging.rst index bf84fecc..33578d16 100644 --- a/sources/pyside6/doc/tutorials/debugging/qml_debugging.rst +++ b/sources/pyside6/doc/tutorials/debugging/qml_debugging.rst @@ -1,8 +1,10 @@ .. _tutorial_qml_debugging: +Mixed mode Debugging of PySide6 QML Applications +************************************************ Using Qt Creator's QML Debugger for a PySide6 QML Application -************************************************************* +============================================================= Besides the C++ debugger, *Qt Creator* provides a `QML debugger`_ which lets you inspect JavaScript code. It works by connecting to a socket server run by the @@ -31,5 +33,28 @@ For instructions on how to use the QML debugger, see .. note:: The code should be removed or disabled when shipping the application as it poses a security risk. +Using the Qt Python VSCode Extension +==================================== + +The `Qt Python extension`_ for Visual Studio Code provides an easier way to debug +PySide6 QML applications with mixed-mode debugging support for both Python and QML. +The extension comes with several preset launch configurations that enable seamless +debugging without manual setup: + +- ``Qt: PySide: Launch`` - Launch and debug PySide6 applications +- ``Qt: PySide: Launch with QML debugger`` - Launch PySide6 applications with QML debugging enabled +- ``Qt: QML: Attach by port`` - Attach the QML debugger to a running application by port number + +With these configurations, you can set breakpoints in both your Python code and QML +files, inspect variables, and step through code execution across the Python-QML boundary. +For mixed Python and QML debugging, you can use a compound configuration that combines +``Qt: PySide: Launch with QML debugger`` and ``Qt: QML: Attach by port`` to debug both +layers simultaneously. + +For detailed instructions on how to debug PySide6 applications using the Qt Python +extension, see `Debugging Qt for Python Applications in VSCode`_. + .. _`QML debugger`: https://doc.qt.io/qtcreator/creator-debugging-qml.html .. _`Debugging a Qt Quick Example Application`: https://doc.qt.io/qtcreator/creator-qml-debugging-example.html +.. _`Qt Python extension`: https://marketplace.visualstudio.com/items?itemName=TheQtCompany.qt-python +.. _`Debugging Qt for Python Applications in VSCode`: https://doc-snapshots.qt.io/vscodeext-dev/vscodeext-how-debug-apps-python.html diff --git a/sources/pyside6/doc/tutorials/drumpad/index.md b/sources/pyside6/doc/tutorials/drumpad/index.md new file mode 100644 index 00000000..3e90c880 --- /dev/null +++ b/sources/pyside6/doc/tutorials/drumpad/index.md @@ -0,0 +1,215 @@ +(tutorial_qt_design_studio_integration)= + +# Qt Design Studio integration tutorial + +## Summary + +This tutorial provides a step-by-step guide for exporting a [Qt Design Studio] project for Python +development and deployment. You will learn how to: + + - Export a Qt Design Studio in order to get a project template for further Python development + - Implement custom QML elements using Python + - Successfully deploy the PySide6 application + +```{note} +This tutorial is not focused on teaching how to use Qt Design Studio or QML, but rather how to +integrate PySide6 with an existing Qt Design Studio project. If you want to learn how to use Qt +Design Studio, check the [available tutorials][qt-design-studio-tutorials]. +``` + +The project consists in a single "drumpad" screen that can be used to play different sound effects. +The screen is composed of a responsive grid of buttons, each playing a different sound. In addition, +a waveform display shows the audio amplitude over time using [Qt Multimedia] features. + +![Drumpad example screenshot](resources/drumpad.png) + +## Workflow overview + +Before starting the tutorial, we need to understand the Qt Design Studio project workflow first. + +1. **Create a QML project using Qt Design Studio**: Develop the application UI in a user +friendly way. You can visually design components, screens and animations without writing QML code +manually. + +2. **Export the project**: Create a Python project using the Qt Design Studio generator. + +3. **Develop logic**: Implement custom functionalities and application logic in Python, connecting +it to the exported QML files. Define *backend* elements and signal communication with the UI. + +4. **Deploy**: Package the application into a standalone executable using the [pyside6-deploy] tool. +This bundles all required dependencies, resources, and modules into a distributable format. + +## Qt Design Studio project set up + +The initial project source code is available for download at {ref}`example_tutorials_drumpad_initial_project`. +This provides the starting point for the tutorial and includes a set of QML files, Qt Resource +files, and other project files. + +![Qt Design Studio showing the main screen](resources/design_studio_main_screen.png) + +Qt Design Studio offers a Python project template generator. The option can be enabled in the +`File` > `Export project` > `Enable Python Generator` setting. + +![Qt Design Studio Enable Python Generator setting](resources/design_studio_enable_python_generator.png) + +When the setting is enabled, Qt Design Studio will create a `Python` folder in the project directory, +containing the `main.py` and `pyproject.toml` files as well as the `autogen` folder. The `autogen` +folder contains the `settings.py` file, which is used to set up the project root path, the QML +import paths and other Qt specific settings. + +## Python development + +The project contains three Python files that define QML elements located in the `Python/audio` +folder. They belong to the `Audio` QML module. The QML code expects that they exist. Otherwise, the +application can not be executed. + +The `AudioEngine` QML element is responsible for playing audio files. It uses the `QSoundEffect` +class from the [Qt Multimedia] module to play the audio files. It also provides [Qt Signals] for +communicating with the QML layer. + +
+audio_engine.py + +```{literalinclude} ../../../../../../../../examples/tutorials/drumpad/final_project/Python/audio/audio_engine.py +--- +language: python +caption: audio_engine.py +linenos: true +--- +``` +
+ +The `AudioFilesModel` QML element is responsible for managing the audio files. It fetches the +available audio files from the `Sounds` folder and provides a `getModel()` method to return a list +of files. It detects whether the application has been deployed because the compiled Qt resource +files are used in this case. + +
+audio_files_model.py + +```{literalinclude} ../../../../../../../../examples/tutorials/drumpad/final_project/Python/audio/audio_files_model.py +--- +language: python +caption: audio_files_model.py +linenos: true +--- +``` +
+ +The `WaveformItem` QML element is responsible for displaying the audio waveform. It uses the +`QAudioDecoder` and `QAudioFormat` classes from the [Qt Multimedia] module to decode the audio file +and display the waveform. The graph is drawn using [QPainter]. + +
+waveform_item.py + +```{literalinclude} ../../../../../../../../examples/tutorials/drumpad/final_project/Python/audio/waveform_item.py +--- +language: python +caption: waveform_item.py +linenos: true +--- +``` +
+ +## Running the application + +Navigate to the `Python/` directory of the project: + +```bash +cd Python/ +``` + +Then, build the project using: + +```bash +pyside6-project build +``` + +This command will compile resources, UI files, QML files, and other necessary components. + +## Deployment + +In order to create a standalone executable of the application, we can use the [pyside6-deploy] +command line tool. It will analyze the project source code, determine the required Qt modules and +dependencies and bundle the code into a native executable. + +To deploy the application, execute the following command from the `Python/` directory: + +```bash +pyside6-deploy --name Drumpad +``` + +This will create a standalone executable for the application in the project directory. + +```{important} +Make sure to fulfil the [pyside6-deploy requirements] for your platform. Otherwise, the tool will +not detect that the example code uses Qt Multimedia module. In that case, the produced +executable will not work properly. +``` + +### Qt resource files + +Note that since the `main.py` file is contained in the `Python` folder, its references to the project +QML files and other resources have to traverse one level up. When the project is deployed, this is +an issue because of the way [Nuitka] works. After the deployment, the `main.py` entry point file +is morphed into a native executable file, but its location in the project folder changes: + +Project structure before deployment: +``` +├── Drumpad +│ ├── AvailableSoundsComboBox.qml +│ ... +├── Python +│ ├── main.py +│ ├── pyproject.toml +│ └── autogen +└── Sounds + ├── Clap.wav + ├── Kick Drum.wav + ... +``` + +Project structure after deployment: +``` +├── main.exe (OS dependent executable format) +├── Drumpad +│ ├── AvailableSoundsComboBox.qml +│ ... +└── Sounds + ├── Clap.wav + ├── Kick Drum.wav + ... +``` + +The relative location of the resources changes after the deployment. For example, before deploying +the application, the path for accessing a sound from the `main.py` file would be: +`../Sounds/Clap.wav`. After the deployment, the relative path is now: `Sounds/Clap.wav`. + +This issue is addressed by the [pyside6-deploy] tool thanks to the usage of +[Qt resource files][Qt Resource System]. All the files listed in the `Drumpad.qrc` file are embedded +in the executable and can be accessed by importing the `Python/autogen/resources.py` Python file. +This way, the paths can be easily resolved properly after the deployment of the application. + +Qt Design Studio creates the `Python/autogen/settings.py` file which contains code that enables the +usage of the compiled Qt resources when the application is deployed. This code can be modified on +demand. + +## Conclusion + +In this tutorial, you learned how to integrate a user interface developed in [Qt Design Studio] with +a Python *backend* using PySide6. We walked through the complete workflow, from exporting a QML +project and implementing custom Python logic to packaging the application into a standalone +executable using [pyside6-deploy]. + +[Qt Design Studio]: https://www.qt.io/product/ui-design-tools/ +[Qt Quick]: https://doc.qt.io/qt-6/qtquick-index.html +[qt-design-studio-tutorials]: https://doc.qt.io/qtdesignstudio/gstutorials.html +[Qt Multimedia]: https://doc.qt.io/qt-6/qtmultimedia-index.html +[Nuitka]: https://nuitka.net/ +[Qt Resource System]: https://doc.qt.io/qt-6/resources.html +[pyside6-deploy]: https://doc.qt.io/qtforpython-6/deployment/deployment-pyside6-deploy.html +[pyside6-deploy requirements]: https://doc.qt.io/qtforpython-6/deployment/deployment-pyside6-deploy.html#considerations +[QML module]: https://doc.qt.io/qt-6/qtqml-modules-topic.html +[Qt Signals]: https://doc.qt.io/qt-6/signalsandslots.html +[QPainter]: https://doc.qt.io/qt-6/qpainter.html diff --git a/sources/pyside6/doc/tutorials/drumpad/resources/design_studio_enable_python_generator.png b/sources/pyside6/doc/tutorials/drumpad/resources/design_studio_enable_python_generator.png new file mode 100644 index 00000000..e3772269 Binary files /dev/null and b/sources/pyside6/doc/tutorials/drumpad/resources/design_studio_enable_python_generator.png differ diff --git a/sources/pyside6/doc/tutorials/drumpad/resources/design_studio_main_screen.png b/sources/pyside6/doc/tutorials/drumpad/resources/design_studio_main_screen.png new file mode 100644 index 00000000..cfba6936 Binary files /dev/null and b/sources/pyside6/doc/tutorials/drumpad/resources/design_studio_main_screen.png differ diff --git a/sources/pyside6/doc/tutorials/drumpad/resources/drumpad.png b/sources/pyside6/doc/tutorials/drumpad/resources/drumpad.png new file mode 100644 index 00000000..46b0142d Binary files /dev/null and b/sources/pyside6/doc/tutorials/drumpad/resources/drumpad.png differ diff --git a/sources/pyside6/doc/tutorials/finance_manager/part1/part1.md b/sources/pyside6/doc/tutorials/finance_manager/part1/part1.md index 6f8b87d2..a5e4976e 100644 --- a/sources/pyside6/doc/tutorials/finance_manager/part1/part1.md +++ b/sources/pyside6/doc/tutorials/finance_manager/part1/part1.md @@ -352,6 +352,7 @@ code: main.py ```{literalinclude} ../../../../../../../../../examples/tutorials/finance_manager/part1/main.py +--- language: python caption: main.py linenos: true diff --git a/sources/pyside6/doc/tutorials/index.rst b/sources/pyside6/doc/tutorials/index.rst index e9349f0c..a06c1b22 100644 --- a/sources/pyside6/doc/tutorials/index.rst +++ b/sources/pyside6/doc/tutorials/index.rst @@ -218,6 +218,33 @@ General Applications expenses/expenses.rst embedded/boot2qt.md +Qt Creator +---------- + +* `Overview: Develop Qt for Python applications`_ +* `Tutorial: Qt Quick and Python`_ +* `Tutorial: Qt Widgets and Python`_ +* `Tutorial: Qt Widgets UI and Python`_ + +Qt Design Studio +---------------- + +.. grid:: 1 3 3 3 + :gutter: 2 + + .. grid-item-card:: Qt Design Studio integration tutorial + :class-item: cover-img + :link: tutorial_qt_design_studio_integration + :link-type: ref + :img-top: drumpad/resources/drumpad.png + + Export a Qt Design Studio project to create a PySide6 application + +.. toctree:: + :hidden: + + drumpad/index.rst + Qt Overviews ------------ @@ -241,3 +268,8 @@ Debug a PySide6 Application debugging/mixed_debugging.rst debugging/qml_debugging.rst + +.. _`Overview: Develop Qt for Python applications`: https://doc.qt.io/qtcreator/creator-python-development.html +.. _`Tutorial: Qt Quick and Python`: https://doc.qt.io/qtcreator/creator-tutorial-python-application-qt-quick.html +.. _`Tutorial: Qt Widgets and Python`: https://doc.qt.io/qtcreator/creator-tutorial-python-application-qt-widgets.html +.. _`Tutorial: Qt Widgets UI and Python`: https://doc.qt.io/qtcreator/creator-tutorial-python-application-qt-widgets-ui.html diff --git a/sources/pyside6/doc/tutorials/qmlapp/main.py b/sources/pyside6/doc/tutorials/qmlapp/main.py index 254d75f1..f52c0848 100644 --- a/sources/pyside6/doc/tutorials/qmlapp/main.py +++ b/sources/pyside6/doc/tutorials/qmlapp/main.py @@ -25,7 +25,7 @@ if __name__ == '__main__': # Set up the application window app = QGuiApplication(sys.argv) view = QQuickView() - view.setResizeMode(QQuickView.SizeRootObjectToView) + view.setResizeMode(QQuickView.ResizeMode.SizeRootObjectToView) # Expose the list to the Qml code my_model = QStringListModel() @@ -38,7 +38,7 @@ if __name__ == '__main__': view.loadFromModule("App", "Main") # Show the window - if view.status() == QQuickView.Error: + if view.status() == QQuickView.Status.Error: sys.exit(-1) view.show() diff --git a/sources/pyside6/doc/tutorials/qmlapp/newpyproject.png b/sources/pyside6/doc/tutorials/qmlapp/newpyproject.png deleted file mode 100644 index 93968a52..00000000 Binary files a/sources/pyside6/doc/tutorials/qmlapp/newpyproject.png and /dev/null differ diff --git a/sources/pyside6/doc/tutorials/qmlapp/newpyproject.webp b/sources/pyside6/doc/tutorials/qmlapp/newpyproject.webp new file mode 100644 index 00000000..dea2e940 Binary files /dev/null and b/sources/pyside6/doc/tutorials/qmlapp/newpyproject.webp differ diff --git a/sources/pyside6/doc/tutorials/qmlapp/projectsmode.png b/sources/pyside6/doc/tutorials/qmlapp/projectsmode.png deleted file mode 100644 index c66d8872..00000000 Binary files a/sources/pyside6/doc/tutorials/qmlapp/projectsmode.png and /dev/null differ diff --git a/sources/pyside6/doc/tutorials/qmlapp/projectsmode.webp b/sources/pyside6/doc/tutorials/qmlapp/projectsmode.webp new file mode 100644 index 00000000..bbd20975 Binary files /dev/null and b/sources/pyside6/doc/tutorials/qmlapp/projectsmode.webp differ diff --git a/sources/pyside6/doc/tutorials/qmlapp/pyproject.toml b/sources/pyside6/doc/tutorials/qmlapp/pyproject.toml new file mode 100644 index 00000000..f88f386c --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlapp/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "qml-application" + +[tool.pyside6-project] +files = ["main.py", "App/Main.qml", "App/logo.png", "App/qmldir"] diff --git a/sources/pyside6/doc/tutorials/qmlapp/pyprojname.png b/sources/pyside6/doc/tutorials/qmlapp/pyprojname.png deleted file mode 100644 index 98328074..00000000 Binary files a/sources/pyside6/doc/tutorials/qmlapp/pyprojname.png and /dev/null differ diff --git a/sources/pyside6/doc/tutorials/qmlapp/pyprojname.webp b/sources/pyside6/doc/tutorials/qmlapp/pyprojname.webp new file mode 100644 index 00000000..49db6f19 Binary files /dev/null and b/sources/pyside6/doc/tutorials/qmlapp/pyprojname.webp differ diff --git a/sources/pyside6/doc/tutorials/qmlapp/pyprojxplor.png b/sources/pyside6/doc/tutorials/qmlapp/pyprojxplor.png deleted file mode 100644 index e01e2ebe..00000000 Binary files a/sources/pyside6/doc/tutorials/qmlapp/pyprojxplor.png and /dev/null differ diff --git a/sources/pyside6/doc/tutorials/qmlapp/pyprojxplor.webp b/sources/pyside6/doc/tutorials/qmlapp/pyprojxplor.webp new file mode 100644 index 00000000..57c534e0 Binary files /dev/null and b/sources/pyside6/doc/tutorials/qmlapp/pyprojxplor.webp differ diff --git a/sources/pyside6/doc/tutorials/qmlapp/qmlapplication.rst b/sources/pyside6/doc/tutorials/qmlapp/qmlapplication.rst index 9dd3e434..fac8ae13 100644 --- a/sources/pyside6/doc/tutorials/qmlapp/qmlapplication.rst +++ b/sources/pyside6/doc/tutorials/qmlapp/qmlapplication.rst @@ -14,13 +14,12 @@ In this tutorial, you'll also learn how to provide data from Python as a QML initial property, which is then consumed by the ListView defined in the QML file. -Before you begin, install the following prerequisites: - -* The `PySide6 `_ Python packages. -* *Qt Creator* from - `https://download.qt.io - `_. +Before you begin, install *Qt Creator* from +`https://download.qt.io `_. +`Develop Qt for Python applications`_ describes how Python installations +are handled by *Qt Creator*. By default, *Qt Creator* will prompt you +to install PySide6 at the appropriate places. The following step-by-step instructions guide you through application development process using *Qt Creator*: @@ -28,35 +27,37 @@ development process using *Qt Creator*: #. Open *Qt Creator* and select **File > New File or Project..** menu item to open following dialog: - .. image:: newpyproject.png + .. image:: newpyproject.webp #. Select **Qt for Python - Empty** from the list of application templates and select **Choose**. - .. image:: pyprojname.png + .. image:: pyprojname.webp #. Give a **Name** to your project, choose its location in the filesystem, and select **Finish** to create an empty ``main.py`` - and ``main.pyproject``. + and ``pyproject.toml``. - .. image:: pyprojxplor.png + .. image:: pyprojxplor.webp - This should create a ``main.py`` and ```main.pyproject`` files + This should create a ``main.py`` and ```pyproject.toml`` files for the project. #. Download :download:`Main.qml`, :download:`qmldir` and :download:`logo.png ` and place them in a subdirectory named `App` in your project folder. This creates a basic QML module. -#. Double-click on ``main.pyproject`` to open it in edit mode, and append +#. Double-click on ``pyproject.toml``` to open it in edit mode, and append ``view.qml`` and ``logo.png`` to the **files** list. This is how your project file should look after this change: .. code:: - { - "files": ["main.py", "App/Main.qml", "App/logo.png", "App/qmldir"] - } + [project] + name = "qml-application" + + [tool.pyside6-project] + files = ["main.py", "App/Main.qml", "App/logo.png", "App/qmldir"] #. Now that you have the necessary bits for the application, import the Python modules in your ``main.py``, and download country data and @@ -107,29 +108,19 @@ development process using *Qt Creator*: #. Your application is ready to be run now. Select **Projects** mode to choose the Python version to run it. - .. image:: projectsmode.png + .. image:: projectsmode.webp Run the application by using the ``CTRL+R`` keyboard shortcut to see if it looks like this: .. image:: qmlapplication.png -You could also watch the following video tutorial for guidance to develop -this application: - -.. raw:: html - -
- -
- ******************** Related information ******************** * `QML Reference `_ +* `Develop Qt for Python applications `_ * :doc:`../qmlintegration/qmlintegration` + +.. _`Develop Qt for Python applications`: https://doc.qt.io/qtcreator/creator-python-development.html diff --git a/sources/pyside6/libpyside/CMakeLists.txt b/sources/pyside6/libpyside/CMakeLists.txt index 539f1f32..b0050dd0 100644 --- a/sources/pyside6/libpyside/CMakeLists.txt +++ b/sources/pyside6/libpyside/CMakeLists.txt @@ -40,6 +40,7 @@ set(libpyside_HEADERS # installed below pysideslot_p.h pysidestaticstrings.h pysideutils.h + pysidevariantutils.h pysideweakref.h qobjectconnect.h signalmanager.h @@ -59,6 +60,7 @@ set(libpyside_SRC pysidesignal.cpp pysideslot.cpp pysideproperty.cpp + pysidevariantutils.cpp pysideweakref.cpp pyside.cpp pyside_numpy.cpp @@ -99,7 +101,7 @@ append_size_optimization_flags(pyside6) target_include_directories(pyside6 PUBLIC $ - $ + $ ) target_compile_definitions(pyside6 PRIVATE -DQT_LEAN_HEADERS=1 -DQT_NO_KEYWORDS=1) @@ -144,28 +146,29 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/pyside6.pc.in" include(CMakePackageConfigHelpers) # Build-tree / super project package config file. -set(PYSIDE_PYTHONPATH "${pysidebindings_BINARY_DIR}/PySide6") -set(PYSIDE_TYPESYSTEMS "${pysidebindings_SOURCE_DIR}/PySide6/templates/") -set(PYSIDE_GLUE "${pysidebindings_SOURCE_DIR}/PySide6/glue") +set(PYSIDE_PYTHONPATH "${PYTHON_SITE_PACKAGES}/PySide6") +set(PYSIDE_TYPESYSTEMS "${CMAKE_INSTALL_PREFIX}/share/PySide6${pyside6_SUFFIX}/typesystems") +set(PYSIDE_GLUE "${CMAKE_INSTALL_PREFIX}/share/PySide6${pyside6_SUFFIX}/glue") configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/PySide6Config-spec.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/PySide6Config${SHIBOKEN_PYTHON_CONFIG_SUFFIX}.cmake" - INSTALL_DESTINATION "${CMAKE_CURRENT_BINARY_DIR}" - PATH_VARS PYSIDE_PYTHONPATH PYSIDE_TYPESYSTEMS PYSIDE_GLUE - INSTALL_PREFIX "${CMAKE_CURRENT_BINARY_DIR}" + INSTALL_DESTINATION "${CMAKE_CURRENT_BINARY_DIR}" + PATH_VARS PYSIDE_PYTHONPATH PYSIDE_TYPESYSTEMS PYSIDE_GLUE + INSTALL_PREFIX "${CMAKE_CURRENT_BINARY_DIR}" ) -set(PYSIDE_PYTHONPATH "${PYTHON_SITE_PACKAGES}/PySide6") -set(PYSIDE_TYPESYSTEMS "${CMAKE_INSTALL_PREFIX}/share/PySide6${pyside6_SUFFIX}/typesystems") -set(PYSIDE_GLUE "${CMAKE_INSTALL_PREFIX}/share/PySide6${pyside6_SUFFIX}/glue") +# Install-tree / wheel configuration +set(PYSIDE_PYTHONPATH "") +set(PYSIDE_TYPESYSTEMS "typesystems") +set(PYSIDE_GLUE "glue") +set(PYSIDE_SOVERSION "${pyside6_library_so_version}") -# Install-tree / relocatable package config file. configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/PySide6Config-spec.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/install/PySide6Config${SHIBOKEN_PYTHON_CONFIG_SUFFIX}.cmake" - INSTALL_DESTINATION "${LIB_INSTALL_DIR}/cmake/PySide6" - PATH_VARS PYSIDE_PYTHONPATH PYSIDE_TYPESYSTEMS PYSIDE_GLUE + INSTALL_DESTINATION "${LIB_INSTALL_DIR}/cmake/PySide6" + PATH_VARS PYSIDE_PYTHONPATH PYSIDE_TYPESYSTEMS PYSIDE_GLUE ) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/PySide6Config.cmake.in" @@ -174,14 +177,24 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/PySide6ConfigVersion.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/PySide6ConfigVersion.cmake" @ONLY) install(FILES ${libpyside_HEADERS} - DESTINATION include/${BINDING_NAME}${pyside6_SUFFIX}) + DESTINATION ${BINDING_NAME}${pyside6_SUFFIX}/include) +# build-time installation install(TARGETS pyside6 EXPORT PySide6Targets LIBRARY DESTINATION "${LIB_INSTALL_DIR}" ARCHIVE DESTINATION "${LIB_INSTALL_DIR}" RUNTIME DESTINATION bin) -install(EXPORT PySide6Targets NAMESPACE PySide6:: - DESTINATION "${LIB_INSTALL_DIR}/cmake/PySide6") + +# wheel installation +set_target_properties(pyside6 PROPERTIES + VERSION ${PYSIDE_SOVERSION}) + +if(NOT is_pyside6_superproject_build) + install(TARGETS pyside6 EXPORT PySide6WheelTargets + LIBRARY DESTINATION "PySide6" + ARCHIVE DESTINATION "PySide6" + RUNTIME DESTINATION "PySide6") +endif() install(FILES "${CMAKE_CURRENT_BINARY_DIR}/pyside6${pyside6_SUFFIX}.pc" DESTINATION "${LIB_INSTALL_DIR}/pkgconfig") diff --git a/sources/pyside6/libpyside/PySide6Config-spec.cmake.in b/sources/pyside6/libpyside/PySide6Config-spec.cmake.in index fd3de1e7..f9e9a490 100644 --- a/sources/pyside6/libpyside/PySide6Config-spec.cmake.in +++ b/sources/pyside6/libpyside/PySide6Config-spec.cmake.in @@ -10,6 +10,10 @@ if (NOT TARGET PySide6::pyside6) include("${CMAKE_CURRENT_LIST_DIR}/PySide6Targets.cmake") endif() +# set static variables +set(PYSIDE_PYTHON_CONFIG_SUFFIX "@PYTHON_CONFIG_SUFFIX@") +set(PYSIDE_SO_VERSION "@pyside6_library_so_version@") + # Set relocatable variables. set_and_check(PYSIDE_PYTHONPATH "@PACKAGE_PYSIDE_PYTHONPATH@") set_and_check(PYSIDE_TYPESYSTEMS "@PACKAGE_PYSIDE_TYPESYSTEMS@") diff --git a/sources/pyside6/libpyside/class_property.cpp b/sources/pyside6/libpyside/class_property.cpp index 89320977..2e1001be 100644 --- a/sources/pyside6/libpyside/class_property.cpp +++ b/sources/pyside6/libpyside/class_property.cpp @@ -168,12 +168,13 @@ void init(PyObject *module) PyTypeObject *type = SbkObjectType_TypeF(); type->tp_setattro = SbkObjectType_meta_setattro; - if (InitSignatureStrings(PyClassProperty_TypeF(), PyClassProperty_SignatureStrings) < 0) + auto *classPropertyType = PyClassProperty_TypeF(); + if (InitSignatureStrings(classPropertyType, PyClassProperty_SignatureStrings) < 0) return; - Py_INCREF(PyClassProperty_TypeF()); - auto *classproptype = reinterpret_cast(PyClassProperty_TypeF()); - PyModule_AddObject(module, "PyClassProperty", classproptype); + auto *obClassPropertyType = reinterpret_cast(classPropertyType); + Py_INCREF(obClassPropertyType); + PepModule_AddType(module, classPropertyType); } } // namespace PySide::ClassProperty diff --git a/sources/pyside6/libpyside/dynamicqmetaobject.cpp b/sources/pyside6/libpyside/dynamicqmetaobject.cpp index a3b718d8..f92541d5 100644 --- a/sources/pyside6/libpyside/dynamicqmetaobject.cpp +++ b/sources/pyside6/libpyside/dynamicqmetaobject.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -24,12 +25,24 @@ #include #include +#include #include using namespace Qt::StringLiterals; using namespace PySide; +// QMetaEnum can handle quint64 or int values. Check for big long values and force +// them to quint64 (long=64bit/int=32bit on Linux vs long=32bit on Windows). +// Note: underflows are currently not handled well. +static QVariant longToEnumValue(PyObject *value) +{ + int overflow{}; + const long longValue = PyLong_AsLongAndOverflow(value, &overflow); + return overflow != 0 || longValue > std::numeric_limits::max() + ? QVariant(PyLong_AsUnsignedLongLong(value)) : QVariant(int(longValue)); +} + // MetaObjectBuilder: Provides the QMetaObject's returned by // QObject::metaObject() for PySide6 objects. There are several // scenarios to consider: @@ -63,8 +76,8 @@ public: int addProperty(const QByteArray &property, PyObject *data); void addInfo(const QByteArray &key, const QByteArray &value); void addInfo(const QMap &info); - void addEnumerator(const char *name, bool flag, bool scoped, - const MetaObjectBuilder::EnumValues &entries); + QMetaEnumBuilder addEnumerator(const char *name, bool flag, bool scoped, + const MetaObjectBuilder::EnumValues &entries); void removeProperty(int index); const QMetaObject *update(); @@ -89,31 +102,21 @@ QMetaObjectBuilder *MetaObjectBuilderPrivate::ensureBuilder() return m_builder; } -MetaObjectBuilder::MetaObjectBuilder(const char *className, const QMetaObject *metaObject) : - m_d(new MetaObjectBuilderPrivate) +MetaObjectBuilder::MetaObjectBuilder(const QMetaObject *metaObject) + : m_d(new MetaObjectBuilderPrivate) { m_d->m_baseObject = metaObject; - m_d->m_builder = new QMetaObjectBuilder(); - m_d->m_builder->setClassName(className); - m_d->m_builder->setSuperClass(metaObject); - m_d->m_builder->setClassName(className); } +// Parse the type in case of a Python class inheriting a Qt class. MetaObjectBuilder::MetaObjectBuilder(PyTypeObject *type, const QMetaObject *metaObject) : m_d(new MetaObjectBuilderPrivate) { m_d->m_baseObject = metaObject; - const char *className = type->tp_name; - if (const char *lastDot = strrchr(type->tp_name, '.')) - className = lastDot + 1; - // Different names indicate a Python class inheriting a Qt class. - // Parse the type. - if (strcmp(className, metaObject->className()) != 0) { - m_d->m_builder = new QMetaObjectBuilder(); - m_d->m_builder->setClassName(className); - m_d->m_builder->setSuperClass(metaObject); - m_d->parsePythonType(type); - } + m_d->m_builder = new QMetaObjectBuilder(); + m_d->m_builder->setClassName(PepType_GetNameStr(type)); + m_d->m_builder->setSuperClass(metaObject); + m_d->parsePythonType(type); } MetaObjectBuilder::~MetaObjectBuilder() @@ -303,9 +306,9 @@ QMetaPropertyBuilder auto *typeObject = Property::getTypeObject(property); if (typeObject != nullptr && PyType_Check(typeObject)) { auto *pyTypeObject = reinterpret_cast(typeObject); - if (qstrncmp(pyTypeObject->tp_name, "PySide", 6) != 0 + if (qstrncmp(PepType_GetFullyQualifiedNameStr(pyTypeObject), "PySide", 6) != 0 && PySide::isQObjectDerived(pyTypeObject, false)) { - const QByteArray pyType(pyTypeObject->tp_name); + const QByteArray pyType(PepType_GetFullyQualifiedNameStr(pyTypeObject)); const auto metaType = QMetaType::fromName(pyType + '*'); if (metaType.isValid()) { return builder->addProperty(propertyName, pyType, @@ -384,8 +387,9 @@ void MetaObjectBuilder::addEnumerator(const char *name, bool flag, bool scoped, m_d->addEnumerator(name, flag, scoped, entries); } -void MetaObjectBuilderPrivate::addEnumerator(const char *name, bool flag, bool scoped, - const MetaObjectBuilder::EnumValues &entries) +QMetaEnumBuilder + MetaObjectBuilderPrivate::addEnumerator(const char *name, bool flag, bool scoped, + const MetaObjectBuilder::EnumValues &entries) { auto *builder = ensureBuilder(); int have_already = builder->indexOfEnumerator(name); @@ -394,10 +398,15 @@ void MetaObjectBuilderPrivate::addEnumerator(const char *name, bool flag, bool s auto enumbuilder = builder->addEnumerator(name); enumbuilder.setIsFlag(flag); enumbuilder.setIsScoped(scoped); + for (const auto &item : entries) { + if (item.second.typeId() == QMetaType::ULongLong) + enumbuilder.addKey(item.first, item.second.toULongLong()); + else + enumbuilder.addKey(item.first, item.second.toInt()); + } - for (const auto &item : entries) - enumbuilder.addKey(item.first, item.second); m_dirty = true; + return enumbuilder; } void MetaObjectBuilderPrivate::removeProperty(int index) @@ -676,16 +685,26 @@ void MetaObjectBuilderPrivate::parsePythonType(PyTypeObject *type) AutoDecRef items(PyMapping_Items(members)); Py_ssize_t nr_items = PySequence_Length(items); - QList > entries; + MetaObjectBuilder::EnumValues entries; + entries.reserve(nr_items); + bool is64bit = false; for (Py_ssize_t idx = 0; idx < nr_items; ++idx) { AutoDecRef item(PySequence_GetItem(items, idx)); AutoDecRef key(PySequence_GetItem(item, 0)); AutoDecRef member(PySequence_GetItem(item, 1)); AutoDecRef value(PyObject_GetAttr(member, Shiboken::PyName::value())); const auto *ckey = String::toCString(key); - auto ivalue = PyLong_AsSsize_t(value); - entries.push_back(std::make_pair(ckey, int(ivalue))); + QVariant valueV = longToEnumValue(value.object()); + if (valueV.typeId() == QMetaType::ULongLong) + is64bit = true; + entries.append(std::make_pair(QByteArray(ckey), valueV)); } - addEnumerator(name, isFlag, true, entries); + auto enumBuilder = addEnumerator(name, isFlag, true, entries); + QByteArray qualifiedName = ensureBuilder()->className() + "::"_ba + name; + auto *typeObject = reinterpret_cast(obEnumType); + auto metaType = is64bit + ? PySide::QEnum::createGenericEnum64MetaType(qualifiedName, typeObject) + : PySide::QEnum::createGenericEnumMetaType(qualifiedName, typeObject); + enumBuilder.setMetaType(metaType); } } diff --git a/sources/pyside6/libpyside/dynamicqmetaobject.h b/sources/pyside6/libpyside/dynamicqmetaobject.h index fd5a5f55..2b85dc6d 100644 --- a/sources/pyside6/libpyside/dynamicqmetaobject.h +++ b/sources/pyside6/libpyside/dynamicqmetaobject.h @@ -9,6 +9,7 @@ #include #include +#include #include @@ -17,16 +18,17 @@ class MetaObjectBuilderPrivate; namespace PySide { -class MetaObjectBuilder +class PYSIDE_API MetaObjectBuilder { Q_DISABLE_COPY_MOVE(MetaObjectBuilder) public: - using EnumValue = std::pair; + using EnumValue = std::pair; // Int/ULongLong using EnumValues = QList; - MetaObjectBuilder(const char *className, const QMetaObject *metaObject); - - MetaObjectBuilder(PyTypeObject *type, const QMetaObject *metaObject); + // Plain wrapped Qt types + explicit MetaObjectBuilder(const QMetaObject *metaObject); + // Types defined in Python which are parsed + explicit MetaObjectBuilder(PyTypeObject *type, const QMetaObject *metaObject); ~MetaObjectBuilder(); int indexOfMethod(QMetaMethod::MethodType mtype, const QByteArray &signature) const; @@ -44,7 +46,7 @@ public: const QMetaObject *update(); - PYSIDE_API static QString formatMetaObject(const QMetaObject *metaObject); + static QString formatMetaObject(const QMetaObject *metaObject); private: MetaObjectBuilderPrivate *m_d; diff --git a/sources/pyside6/libpyside/dynamicslot.cpp b/sources/pyside6/libpyside/dynamicslot.cpp index 1af28642..3d9b9c1b 100644 --- a/sources/pyside6/libpyside/dynamicslot.cpp +++ b/sources/pyside6/libpyside/dynamicslot.cpp @@ -17,6 +17,7 @@ #include #include #include +#include namespace PySide { @@ -314,6 +315,7 @@ public: public Q_SLOTS: void senderDestroyed(QObject *o); + void reparentOnQApp(); }; void SenderSignalDeletionTracker::senderDestroyed(QObject *o) @@ -327,6 +329,12 @@ void SenderSignalDeletionTracker::senderDestroyed(QObject *o) } } +void SenderSignalDeletionTracker::reparentOnQApp() +{ + if (auto *app = QCoreApplication::instance()) + setParent(app); +} + static QPointer senderSignalDeletionTracker; static void disconnectReceiver(PyObject *pythonSelf) @@ -364,7 +372,15 @@ void registerSlotConnection(QObject *source, int signalIndex, PyObject *callback connectionHash.insert(connectionKey(source, signalIndex, callback), connection); if (senderSignalDeletionTracker.isNull()) { auto *app = QCoreApplication::instance(); - senderSignalDeletionTracker = new SenderSignalDeletionTracker(app); + if (app == nullptr || QThread::currentThread() == app->thread()) { + senderSignalDeletionTracker = new SenderSignalDeletionTracker(app); + } else { + senderSignalDeletionTracker = new SenderSignalDeletionTracker(nullptr); + senderSignalDeletionTracker->moveToThread(app->thread()); + senderSignalDeletionTracker->metaObject()->invokeMethod(senderSignalDeletionTracker, + "reparentOnQApp", + Qt::QueuedConnection); + } Py_AtExit(clearConnectionHash); } diff --git a/sources/pyside6/libpyside/feature_select.cpp b/sources/pyside6/libpyside/feature_select.cpp index a60dd331..60659b2d 100644 --- a/sources/pyside6/libpyside/feature_select.cpp +++ b/sources/pyside6/libpyside/feature_select.cpp @@ -8,12 +8,15 @@ #include #include +#include #include #include #include #include +#include + ////////////////////////////////////////////////////////////////////////////// // // PYSIDE-1019: Support switchable extensions @@ -123,7 +126,7 @@ static void ensureNewDictType() if (new_dict_type == nullptr) { new_dict_type = createDerivedDictType(); if (new_dict_type == nullptr) - Py_FatalError("PySide6: Problem creating ChameleonDict"); + Py_FatalError("libshiboken: Problem creating ChameleonDict"); } } @@ -271,19 +274,20 @@ static inline void SelectFeatureSetSubtype(PyTypeObject *type, int select_id) * This is the selector for one sublass. We need to call this for * every subclass until no more subclasses or reaching the wanted id. */ - static const auto *pyTypeType_tp_dict = PepType_GetDict(&PyType_Type); + static auto *pyTypeType_tp_dict = PepType_GetDict(&PyType_Type); AutoDecRef tpDict(PepType_GetDict(type)); if (Py_TYPE(tpDict.object()) == Py_TYPE(pyTypeType_tp_dict)) { // On first touch, we initialize the dynamic naming. // The dict type will be replaced after the first call. if (!replaceClassDict(type)) { - Py_FatalError("failed to replace class dict!"); + Py_FatalError("libshiboken: failed to replace class dict!"); return; } } if (!moveToFeatureSet(type, select_id)) { if (!createNewFeatureSet(type, select_id)) { - Py_FatalError("failed to create a new feature set!"); + PyErr_Print(); + Py_FatalError("libshiboken: failed to create a new feature set!"); return; } } @@ -296,13 +300,12 @@ static inline int getFeatureSelectId() { static auto *undef = PyLong_FromLong(-1); static auto *feature_dict = GetFeatureDict(); - // these things are all borrowed - auto *globals = PyEval_GetGlobals(); - if (globals == nullptr - || globals == cached_globals) + + Shiboken::AutoDecRef globals(PepEval_GetFrameGlobals()); + if (globals.isNull() || globals.object() == cached_globals) return last_select_id; - auto *modname = PyDict_GetItem(globals, PyMagicName::name()); + auto *modname = PyDict_GetItem(globals.object(), PyMagicName::name()); if (modname == nullptr) return last_select_id; @@ -327,12 +330,12 @@ static inline void SelectFeatureSet(PyTypeObject *type) * Generated functions call this directly. * Shiboken will assign it via a public hook of `basewrapper.cpp`. */ - static const auto *pyTypeType_tp_dict = PepType_GetDict(&PyType_Type); + static auto *pyTypeType_tp_dict = PepType_GetDict(&PyType_Type); AutoDecRef tpDict(PepType_GetDict(type)); if (Py_TYPE(tpDict.object()) == Py_TYPE(pyTypeType_tp_dict)) { // We initialize the dynamic features by using our own dict type. if (!replaceClassDict(type)) { - Py_FatalError("failed to replace class dict!"); + Py_FatalError("libshiboken: failed to replace class dict!"); return; } } @@ -445,9 +448,9 @@ static PyObject *methodWithNewName(PyTypeObject *type, * Create a method with a lower case name. */ auto *obtype = reinterpret_cast(type); - const auto len = strlen(new_name); + const auto len = std::strlen(new_name); auto *name = new char[len + 1]; - strcpy(name, new_name); + std::strcpy(name, new_name); auto *new_meth = new PyMethodDef; new_meth->ml_name = name; new_meth->ml_meth = meth->ml_meth; diff --git a/sources/pyside6/libpyside/pyside.cpp b/sources/pyside6/libpyside/pyside.cpp index 195c000d..1f55b30d 100644 --- a/sources/pyside6/libpyside/pyside.cpp +++ b/sources/pyside6/libpyside/pyside.cpp @@ -30,6 +30,8 @@ #include #include #include +#include +#include #include #include #include @@ -63,7 +65,10 @@ using namespace Qt::StringLiterals; static QStack cleanupFunctionList; -static void *qobjectNextAddr; + +// Used by QML (main thread), but needs to be protected against other +// threads constructing QObject's. +static void thread_local *qobjectNextAddr; QT_BEGIN_NAMESPACE extern bool qRegisterResourceData(int, const unsigned char *, const unsigned char *, @@ -410,7 +415,8 @@ static void destructionVisitor(SbkObject *pyObj, void *data) auto *pyQApp = reinterpret_cast(realData[0]); auto *pyQObjectType = reinterpret_cast(realData[1]); - if (pyObj != pyQApp && PyObject_TypeCheck(pyObj, pyQObjectType)) { + auto *ob = reinterpret_cast(pyObj); + if (pyObj != pyQApp && PyObject_TypeCheck(ob, pyQObjectType)) { if (Shiboken::Object::hasOwnership(pyObj) && Shiboken::Object::isValid(pyObj, false)) { Shiboken::Object::setValidCpp(pyObj, false); @@ -452,15 +458,13 @@ std::size_t getSizeOfQObject(PyTypeObject *type) return retrieveTypeUserData(type)->cppObjSize; } -void initDynamicMetaObject(PyTypeObject *type, const QMetaObject *base, std::size_t cppObjSize) +static void initDynamicMetaObjectHelper(PyTypeObject *type, + TypeUserData *userData) { - //create DynamicMetaObject based on python type - auto *userData = new TypeUserData(reinterpret_cast(type), base, cppObjSize); - userData->mo.update(); Shiboken::ObjectType::setTypeUserData(type, userData, Shiboken::callCppDestructor); - //initialize staticQMetaObject property - void *metaObjectPtr = const_cast(userData->mo.update()); + // initialize staticQMetaObject property + const void *metaObjectPtr = userData->mo.update(); static SbkConverter *converter = Shiboken::Conversions::getConverter("QMetaObject"); if (!converter) return; @@ -469,6 +473,11 @@ void initDynamicMetaObject(PyTypeObject *type, const QMetaObject *base, std::siz Shiboken::PyName::qtStaticMetaObject(), pyMetaObject); } +void initDynamicMetaObject(PyTypeObject *type, const QMetaObject *base, std::size_t cppObjSize) +{ + initDynamicMetaObjectHelper(type, new TypeUserData(base, cppObjSize)); +} + TypeUserData *retrieveTypeUserData(PyTypeObject *pyTypeObj) { if (!SbkObjectType_Check(pyTypeObj)) @@ -520,7 +529,9 @@ void initQObjectSubType(PyTypeObject *type, PyObject *args, PyObject * /* kwds * // PYSIDE-1463: Don't change feature selection durin subtype initialization. // This behavior is observed with PySide 6. PySide::Feature::Enable(false); - initDynamicMetaObject(type, userData->mo.update(), userData->cppObjSize); + // create DynamicMetaObject based on python type + auto *subTypeData = new TypeUserData(type, userData->mo.update(), userData->cppObjSize); + initDynamicMetaObjectHelper(type, subTypeData); PySide::Feature::Enable(true); } @@ -595,10 +606,7 @@ PyObject *getHiddenDataFromQObject(QObject *cppSelf, PyObject *self, PyObject *n // Search on metaobject (avoid internal attributes started with '__') if (!attr) { - PyObject *type{}; - PyObject *value{}; - PyObject *traceback{}; - PyErr_Fetch(&type, &value, &traceback); // This was omitted for a loong time. + Shiboken::Errors::Stash errorStash; int flags = currentSelectId(Py_TYPE(self)); int snake_flag = flags & 0x01; @@ -623,8 +631,10 @@ PyObject *getHiddenDataFromQObject(QObject *cppSelf, PyObject *self, PyObject *n if (res) { AutoDecRef elemName(PyObject_GetAttr(res, PySideMagicName::name())); // Note: This comparison works because of interned strings. - if (elemName == name) + if (elemName == name) { + errorStash.release(); return res; + } Py_DECREF(res); } PyErr_Clear(); @@ -655,6 +665,7 @@ PyObject *getHiddenDataFromQObject(QObject *cppSelf, PyObject *self, PyObject *n } else if (auto *func = MetaFunction::newObject(cppSelf, i)) { auto *result = reinterpret_cast(func); PyObject_SetAttr(self, name, result); + errorStash.release(); return result; } } @@ -663,17 +674,17 @@ PyObject *getHiddenDataFromQObject(QObject *cppSelf, PyObject *self, PyObject *n auto *pySignal = reinterpret_cast( Signal::newObjectFromMethod(cppSelf, self, signalList)); PyObject_SetAttr(self, name, pySignal); + errorStash.release(); return pySignal; } } - PyErr_Restore(type, value, traceback); } return attr; } bool inherits(PyTypeObject *objType, const char *class_name) { - if (strcmp(objType->tp_name, class_name) == 0) + if (std::strcmp(PepType_GetFullyQualifiedNameStr(objType), class_name) == 0) return true; PyTypeObject *base = objType->tp_base; @@ -1058,10 +1069,10 @@ QMetaType qMetaTypeFromPyType(PyTypeObject *pyType) return QMetaType(QMetaType::Int); if (Shiboken::ObjectType::checkType(pyType)) return QMetaType::fromName(Shiboken::ObjectType::getOriginalName(pyType)); - return QMetaType::fromName(pyType->tp_name); + return QMetaType::fromName(PepType_GetFullyQualifiedNameStr(pyType)); } -debugPyTypeObject::debugPyTypeObject(const PyTypeObject *o) noexcept +debugPyTypeObject::debugPyTypeObject(PyTypeObject *o) noexcept : m_object(o) { } @@ -1073,7 +1084,7 @@ QDebug operator<<(QDebug debug, const debugPyTypeObject &o) debug.nospace(); debug << "PyTypeObject("; if (o.m_object) - debug << '"' << o.m_object->tp_name << '"'; + debug << '"' << PepType_GetFullyQualifiedNameStr(o.m_object) << '"'; else debug << '0'; debug << ')'; @@ -1210,10 +1221,7 @@ QDebug operator<<(QDebug debug, const debugPyObject &o) return debug; } -debugPyBuffer::debugPyBuffer(Py_buffer *b) noexcept : m_buffer(b) -{ -} - +#if !defined(Py_LIMITED_API) || Py_LIMITED_API >= 0x030B0000 static void formatPy_ssizeArray(QDebug &debug, const char *name, const Py_ssize_t *array, int len) { debug << ", " << name << '='; @@ -1227,6 +1235,10 @@ static void formatPy_ssizeArray(QDebug &debug, const char *name, const Py_ssize_ } } +debugPyBuffer::debugPyBuffer(Py_buffer *b) noexcept : m_buffer(b) +{ +} + PYSIDE_API QDebug operator<<(QDebug debug, const debugPyBuffer &b) { QDebugStateSaver saver(debug); @@ -1252,5 +1264,6 @@ PYSIDE_API QDebug operator<<(QDebug debug, const debugPyBuffer &b) debug << ')'; return debug; } +#endif // !Py_LIMITED_API || >= 3.11 } // namespace PySide diff --git a/sources/pyside6/libpyside/pyside6.pc.in b/sources/pyside6/libpyside/pyside6.pc.in index eb45cb9a..65822e6e 100644 --- a/sources/pyside6/libpyside/pyside6.pc.in +++ b/sources/pyside6/libpyside/pyside6.pc.in @@ -1,7 +1,7 @@ prefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=@CMAKE_INSTALL_PREFIX@ libdir=@LIB_INSTALL_DIR@ -includedir=@CMAKE_INSTALL_PREFIX@/include/PySide6@pyside6_SUFFIX@ +includedir=@CMAKE_INSTALL_PREFIX@/PySide6@pyside6_SUFFIX@/include typesystemdir=@CMAKE_INSTALL_PREFIX@/share/PySide6@pyside6_SUFFIX@/typesystems gluedir=@CMAKE_INSTALL_PREFIX@/share/PySide6@pyside6_SUFFIX@/glue pythonpath=@PYTHON_SITE_PACKAGES@ diff --git a/sources/pyside6/libpyside/pyside_p.h b/sources/pyside6/libpyside/pyside_p.h index b13c1829..7033239a 100644 --- a/sources/pyside6/libpyside/pyside_p.h +++ b/sources/pyside6/libpyside/pyside_p.h @@ -14,8 +14,11 @@ namespace PySide // Struct associated with QObject's via Shiboken::Object::getTypeUserData() struct TypeUserData { - explicit TypeUserData(PyTypeObject* type, const QMetaObject* metaobject, std::size_t size) : + explicit TypeUserData(PyTypeObject *type, const QMetaObject *metaobject, std::size_t size) : mo(type, metaobject), cppObjSize(size) {} + // Plain wrapped Qt types + explicit TypeUserData(const QMetaObject *metaobject, std::size_t size) : + mo(metaobject), cppObjSize(size) {} MetaObjectBuilder mo; std::size_t cppObjSize; diff --git a/sources/pyside6/libpyside/pysideclassinfo.cpp b/sources/pyside6/libpyside/pysideclassinfo.cpp index 085e8980..429a9c60 100644 --- a/sources/pyside6/libpyside/pysideclassinfo.cpp +++ b/sources/pyside6/libpyside/pysideclassinfo.cpp @@ -116,8 +116,10 @@ void init(PyObject *module) if (InitSignatureStrings(PySideClassInfo_TypeF(), ClassInfo_SignatureStrings) < 0) return; - Py_INCREF(PySideClassInfo_TypeF()); - PyModule_AddObject(module, "ClassInfo", reinterpret_cast(PySideClassInfo_TypeF())); + auto *classInfoType = PySideClassInfo_TypeF(); + auto *obClassInfoType = reinterpret_cast(classInfoType); + Py_INCREF(obClassInfoType); + PepModule_AddType(module, classInfoType); } bool checkType(PyObject *pyObj) diff --git a/sources/pyside6/libpyside/pysidemetafunction.cpp b/sources/pyside6/libpyside/pysidemetafunction.cpp index 7a496c4b..770146ef 100644 --- a/sources/pyside6/libpyside/pysidemetafunction.cpp +++ b/sources/pyside6/libpyside/pysidemetafunction.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -85,8 +86,10 @@ void init(PyObject *module) if (InitSignatureStrings(PySideMetaFunction_TypeF(), MetaFunction_SignatureStrings) < 0) return; - Py_INCREF(PySideMetaFunction_TypeF()); - PyModule_AddObject(module, "MetaFunction", reinterpret_cast(PySideMetaFunction_TypeF())); + auto *metaFunctionType = PySideMetaFunction_TypeF(); + auto *obMetaFunctionType = reinterpret_cast(metaFunctionType); + Py_INCREF(obMetaFunctionType); + PepModule_AddType(module, metaFunctionType); } PySideMetaFunction *newObject(QObject *source, int methodIndex) diff --git a/sources/pyside6/libpyside/pysideproperty.cpp b/sources/pyside6/libpyside/pysideproperty.cpp index 69d34704..c72bc381 100644 --- a/sources/pyside6/libpyside/pysideproperty.cpp +++ b/sources/pyside6/libpyside/pysideproperty.cpp @@ -46,6 +46,7 @@ static PyObject *qProperty_fdel(PyObject *, void *); static PyMethodDef PySidePropertyMethods[] = { {"getter", reinterpret_cast(qPropertyGetter), METH_O, nullptr}, + // "name@setter" handling {"setter", reinterpret_cast(qPropertySetter), METH_O, nullptr}, {"resetter", reinterpret_cast(qPropertyResetter), METH_O, nullptr}, {"deleter", reinterpret_cast(qPropertyDeleter), METH_O, nullptr}, @@ -114,7 +115,7 @@ PyObject *PySidePropertyPrivate::getValue(PyObject *source) const int PySidePropertyPrivate::setValue(PyObject *source, PyObject *value) { - if (fset && value) { + if (fset != nullptr && fset != Py_None && value != nullptr) { Shiboken::AutoDecRef args(PyTuple_New(2)); PyTuple_SetItem(args, 0, source); PyTuple_SetItem(args, 1, value); @@ -123,7 +124,7 @@ int PySidePropertyPrivate::setValue(PyObject *source, PyObject *value) Shiboken::AutoDecRef result(PyObject_CallObject(fset, args)); return (result.isNull() ? -1 : 0); } - if (fdel) { + if (fdel != nullptr && fdel != Py_None) { Shiboken::AutoDecRef args(PyTuple_New(1)); PyTuple_SetItem(args, 0, source); Py_INCREF(source); @@ -136,7 +137,7 @@ int PySidePropertyPrivate::setValue(PyObject *source, PyObject *value) int PySidePropertyPrivate::reset(PyObject *source) { - if (freset) { + if (freset != nullptr && freset != Py_None) { Shiboken::AutoDecRef args(PyTuple_New(1)); Py_INCREF(source); PyTuple_SetItem(args, 0, source); @@ -486,7 +487,7 @@ static const char *Property_SignatureStrings[] = { "fset:typing.Optional[collections.abc.Callable[[typing.Any,typing.Any],None]]=None," "freset:typing.Optional[collections.abc.Callable[[typing.Any,typing.Any],None]]=None," "doc:str=None," - "notify:typing.Optional[collections.abc.Callable[[],None]]=None," + "notify:typing.Optional[PySide6.QtCore.Signal]=None," "designable:bool=True,scriptable:bool=True," "stored:bool=True,user:bool=False,constant:bool=False,final:bool=False)", "PySide6.QtCore.Property.deleter(self,fdel:collections.abc.Callable[[typing.Any],None])->PySide6.QtCore.Property", @@ -499,11 +500,13 @@ static const char *Property_SignatureStrings[] = { void init(PyObject *module) { - if (InitSignatureStrings(PySideProperty_TypeF(), Property_SignatureStrings) < 0) + auto *propertyType = PySideProperty_TypeF(); + if (InitSignatureStrings(propertyType, Property_SignatureStrings) < 0) return; - Py_INCREF(PySideProperty_TypeF()); - PyModule_AddObject(module, "Property", reinterpret_cast(PySideProperty_TypeF())); + auto *obPropertyType = reinterpret_cast(propertyType); + Py_INCREF(obPropertyType); + PepModule_AddType(module, propertyType); } bool checkType(PyObject *pyObj) @@ -557,12 +560,12 @@ bool isReadable(const PySideProperty * /* self */) bool isWritable(const PySideProperty *self) { - return self->d->fset != nullptr; + return self->d->fset != nullptr && self->d->fset != Py_None; } bool hasReset(const PySideProperty *self) { - return self->d->freset != nullptr; + return self->d->freset != nullptr && self->d->freset != Py_None; } bool isDesignable(const PySideProperty *self) diff --git a/sources/pyside6/libpyside/pysideqenum.cpp b/sources/pyside6/libpyside/pysideqenum.cpp index e922c2d2..166e7778 100644 --- a/sources/pyside6/libpyside/pysideqenum.cpp +++ b/sources/pyside6/libpyside/pysideqenum.cpp @@ -3,12 +3,21 @@ #include "pysideqenum.h" +#include + #include +#include #include +#include #include #include +#include +#include +#include + #include +#include /////////////////////////////////////////////////////////////// // @@ -94,11 +103,65 @@ static bool is_module_code() if (ob_name.isNull()) return false; const char *codename = Shiboken::String::toCString(ob_name); - return strcmp(codename, "") == 0; + return std::strcmp(codename, "") == 0; } } // extern "C" +// Helper code for dynamically creating QMetaType's for @QEnum + +template +static void defaultCtr(const QtPrivate::QMetaTypeInterface *, void *addr) +{ + auto *i = reinterpret_cast(addr); + *i = 0; +} + +template +static void debugOp(const QtPrivate::QMetaTypeInterface *mti, QDebug &debug, const void *addr) +{ + const auto value = *reinterpret_cast(addr); + QDebugStateSaver saver(debug); + debug << mti->name << '('; + if constexpr (std::is_unsigned()) { + debug << Qt::showbase << Qt::hex; + } else { + if (value >= 0) + debug << Qt::showbase << Qt::hex; + } + debug << value << ')'; +} + +template +QMetaType createEnumMetaTypeHelper(const QByteArray &name) +{ + auto *mti = new QtPrivate::QMetaTypeInterface { + 1, // revision + ushort(std::alignment_of()), + sizeof(UnderlyingInt), + uint(QMetaType::fromType().flags() | QMetaType::IsEnumeration), + {}, // typeId + nullptr, // metaObjectFn + qstrdup(name.constData()), + defaultCtr, + nullptr, // copyCtr + nullptr, // moveCtr + nullptr, // dtor + QtPrivate::QEqualityOperatorForType::equals, + QtPrivate::QLessThanOperatorForType::lessThan, + debugOp, + nullptr, // dataStreamOut + nullptr, // dataStreamIn + nullptr // legacyRegisterOp + }; + + QMetaType metaType(mti); + + metaType.id(); // enforce registration + qCDebug(lcPySide, "libpyside: Registering @QEnum meta type \"%s\".", name.constData()); + return metaType; +} + namespace PySide::QEnum { static std::map enumCollector; @@ -193,7 +256,125 @@ std::vector resolveDelayedQEnums(PyTypeObject *containerType) return result; } -} // namespace Shiboken::Enum +QByteArray getTypeName(PyTypeObject *type) +{ + if (!Shiboken::Enum::checkType(type)) + return {}; + + Shiboken::AutoDecRef qualName(PyObject_GetAttr(reinterpret_cast(type), + Shiboken::PyMagicName::qualname())); + QByteArray result = Shiboken::String::toCString(qualName.object()); + result.replace(".", "::"); + + const auto metaType = QMetaType::fromName(result); + return metaType.isValid() && metaType.flags().testFlag(QMetaType::IsEnumeration) + ? result : QByteArray{}; +} + +using GenericEnumType = int; +using GenericEnum64Type = unsigned long long; + +struct GenericEnumRegistry +{ + QList enumTypes; + QList enum64Types; +}; + +Q_GLOBAL_STATIC(GenericEnumRegistry, genericEnumTypeRegistry) + +} // namespace PySide::QEnum + +template +static inline void genericEnumPythonToCppTpl(PyObject *pyIn, void *cppOut) +{ + const auto value = static_cast(Shiboken::Enum::getValue(pyIn)); + *reinterpret_cast(cppOut) = value; +} + +template +static inline PyObject *genericEnumCppToPythonTpl(PyTypeObject *pyType, const void *cppIn) +{ + const auto value = *reinterpret_cast(cppIn); + return Shiboken::Enum::newItem(pyType, value); +} + +extern "C" +{ + +// int +static void genericEnumPythonToCpp(PyObject *pyIn, void *cppOut) +{ + genericEnumPythonToCppTpl(pyIn, cppOut); +} + +static PythonToCppFunc isGenericEnumToCppConvertible(PyObject *pyIn) +{ + + if (PySide::QEnum::genericEnumTypeRegistry()->enumTypes.contains(Py_TYPE(pyIn))) + return genericEnumPythonToCpp; + return {}; +} + +static PyObject *genericEnumCppToPython(PyTypeObject *pyType, const void *cppIn) +{ + return genericEnumCppToPythonTpl(pyType, cppIn); +} + +// unsigned long long +static void genericEnumPythonToCpp64(PyObject *pyIn, void *cppOut) +{ + genericEnumPythonToCppTpl(pyIn, cppOut); +} + +static PythonToCppFunc isGenericEnumToCpp64Convertible(PyObject *pyIn) +{ + + if (PySide::QEnum::genericEnumTypeRegistry()->enum64Types.contains(Py_TYPE(pyIn))) + return genericEnumPythonToCpp64; + return {}; +} + +static PyObject *genericEnumCpp64ToPython(PyTypeObject *pyType, const void *cppIn) +{ + return genericEnumCppToPythonTpl(pyType, cppIn); +} + +} // extern "C" + +namespace PySide::QEnum +{ + +// int +QMetaType createGenericEnumMetaType(const QByteArray &name, PyTypeObject *pyType) +{ + SbkConverter *converter = Shiboken::Conversions::createConverter(pyType, + genericEnumCppToPython); + Shiboken::Conversions::addPythonToCppValueConversion(converter, + genericEnumPythonToCpp, + isGenericEnumToCppConvertible); + Shiboken::Conversions::registerConverterName(converter, name.constData()); + Shiboken::Enum::setTypeConverter(pyType, converter, nullptr); + + genericEnumTypeRegistry->enumTypes.append(pyType); + return createEnumMetaTypeHelper(name); +} + +// "unsigned long long" +QMetaType createGenericEnum64MetaType(const QByteArray &name, PyTypeObject *pyType) +{ + SbkConverter *converter = Shiboken::Conversions::createConverter(pyType, + genericEnumCpp64ToPython); + Shiboken::Conversions::addPythonToCppValueConversion(converter, + genericEnumPythonToCpp64, + isGenericEnumToCpp64Convertible); + Shiboken::Conversions::registerConverterName(converter, name.constData()); + Shiboken::Enum::setTypeConverter(pyType, converter, nullptr); + + genericEnumTypeRegistry()->enum64Types.append(pyType); + return createEnumMetaTypeHelper(name); +} + +} // namespace PySide::QEnum // /////////////////////////////////////////////////////////////// diff --git a/sources/pyside6/libpyside/pysideqenum.h b/sources/pyside6/libpyside/pysideqenum.h index e97db073..82e22e77 100644 --- a/sources/pyside6/libpyside/pysideqenum.h +++ b/sources/pyside6/libpyside/pysideqenum.h @@ -10,6 +10,10 @@ #include +#include + +QT_FORWARD_DECLARE_CLASS(QMetaType) + namespace PySide::QEnum { // PYSIDE-957: Support the QEnum macro @@ -18,6 +22,18 @@ PYSIDE_API int isFlag(PyObject *); PYSIDE_API std::vector resolveDelayedQEnums(PyTypeObject *); PYSIDE_API void init(); + +// PYSIDE-2840: For an enum registered in Qt, return the C++ name. +// Ignore flags here; their underlying enums are of Python type flags anyways. +PYSIDE_API QByteArray getTypeName(PyTypeObject *type); + +// Create a QMetaType for a decorated Python enum (int), enabling +// modification of properties by Qt Widgets Designer. +QMetaType createGenericEnumMetaType(const QByteArray &name, PyTypeObject *pyType); + +// Like createGenericEnumMetaType(), but for "unsigned long long". +QMetaType createGenericEnum64MetaType(const QByteArray &name, PyTypeObject *pyType); + } // namespace PySide::QEnum #endif diff --git a/sources/pyside6/libpyside/pysideqobject.h b/sources/pyside6/libpyside/pysideqobject.h index f81c5039..41762b00 100644 --- a/sources/pyside6/libpyside/pysideqobject.h +++ b/sources/pyside6/libpyside/pysideqobject.h @@ -27,8 +27,10 @@ namespace PySide PYSIDE_API bool fillQtProperties(PyObject *qObj, const QMetaObject *metaObj, PyObject *kwds, bool allowErrors); +/// Initialize the DynamicMetaObject helper for a wrapped Qt type (generated code) PYSIDE_API void initDynamicMetaObject(PyTypeObject *type, const QMetaObject *base, std::size_t cppObjSize); +/// Initialize a Python-derived type PYSIDE_API void initQObjectSubType(PyTypeObject *type, PyObject *args, PyObject *kwds); /// Return the size in bytes of a type that inherits QObject. diff --git a/sources/pyside6/libpyside/pysidesignal.cpp b/sources/pyside6/libpyside/pysidesignal.cpp index 93920dfe..16a7c9c2 100644 --- a/sources/pyside6/libpyside/pysidesignal.cpp +++ b/sources/pyside6/libpyside/pysidesignal.cpp @@ -9,12 +9,14 @@ #include "pysidestaticstrings.h" #include "qobjectconnect.h" #include "signalmanager.h" +#include "pysideqenum.h" #include #include #include #include #include +#include #include #include #include @@ -97,7 +99,7 @@ static bool connection_Check(PyObject *o) static QByteArray typeName = QByteArrayLiteral("PySide") + QByteArray::number(QT_VERSION_MAJOR) + QByteArrayLiteral(".QtCore.QMetaObject.Connection"); - return std::strcmp(o->ob_type->tp_name, typeName.constData()) == 0; + return std::strcmp(PepType_GetFullyQualifiedNameStr(Py_TYPE(o)), typeName.constData()) == 0; } static std::optional parseArgumentNames(PyObject *argArguments) @@ -438,17 +440,17 @@ static FunctionArgumentsResult extractFunctionArgumentsFromSlot(PyObject *slot) // Not retaining a reference inline with what PepFunction_GetName does. Py_DECREF(ret.functionName); - ret.objCode = reinterpret_cast( - PyObject_GetAttr(ret.function, PySide::PySideMagicName::code())); + auto *obObjCode = PyObject_GetAttr(ret.function, PySide::PySideMagicName::code()); + ret.objCode = reinterpret_cast(obObjCode); // Not retaining a reference inline with what PyFunction_GET_CODE does. - Py_XDECREF(ret.objCode); + Py_XDECREF(obObjCode); // Should not happen, but lets handle it gracefully, maybe Nuitka one day // makes these optional, or somebody defined a type named like it without // it being actually being that. if (ret.objCode == nullptr) ret.function = nullptr; - } else if (strcmp(Py_TYPE(slot)->tp_name, "compiled_function") == 0) { + } else if (std::strcmp(PepType_GetFullyQualifiedNameStr(Py_TYPE(slot)), "compiled_function") == 0) { ret.isMethod = false; ret.function = slot; @@ -456,10 +458,10 @@ static FunctionArgumentsResult extractFunctionArgumentsFromSlot(PyObject *slot) // Not retaining a reference inline with what PepFunction_GetName does. Py_DECREF(ret.functionName); - ret.objCode = reinterpret_cast( - PyObject_GetAttr(ret.function, PySide::PySideMagicName::code())); + auto *obObjCode = PyObject_GetAttr(ret.function, PySide::PySideMagicName::code()); + ret.objCode = reinterpret_cast(obObjCode); // Not retaining a reference inline with what PyFunction_GET_CODE does. - Py_XDECREF(ret.objCode); + Py_XDECREF(obObjCode); // Should not happen, but lets handle it gracefully, maybe Nuitka one day // makes these optional, or somebody defined a type named like it without @@ -539,7 +541,8 @@ static PyObject *signalInstanceConnect(PyObject *self, PyObject *args, PyObject return nullptr; Qt::ConnectionType connectionType = Qt::AutoConnection; - if (type != nullptr && qstrcmp(Py_TYPE(type)->tp_name, "ConnectionType") == 0) { + if (type != nullptr + && qstrcmp(PepType_GetFullyQualifiedNameStr(Py_TYPE(type)), "ConnectionType") == 0) { static SbkConverter *connectionTypeConv = Shiboken::Conversions::getConverter("Qt::ConnectionType"); Q_ASSERT(connectionTypeConv); @@ -666,13 +669,9 @@ static PyObject *signalInstanceGetItem(PyObject *self, PyObject *key) static inline void warnDisconnectFailed(PyObject *aSlot, const QByteArray &signature) { if (PyErr_Occurred() != nullptr) { // avoid "%S" invoking str() when an error is set. - PyObject *exc{}; - PyObject *inst{}; - PyObject *tb{}; - PyErr_Fetch(&exc, &inst, &tb); + Shiboken::Errors::Stash errorStash; PyErr_WarnFormat(PyExc_RuntimeWarning, 0, "Failed to disconnect (%s) from signal \"%s\".", Py_TYPE(aSlot)->tp_name, signature.constData()); - PyErr_Restore(exc, inst, tb); } else { PyErr_WarnFormat(PyExc_RuntimeWarning, 0, "Failed to disconnect (%S) from signal \"%s\".", aSlot, signature.constData()); @@ -908,23 +907,26 @@ static const char *SignalInstance_SignatureStrings[] = { void init(PyObject *module) { - if (InitSignatureStrings(PySideMetaSignal_TypeF(), MetaSignal_SignatureStrings) < 0) + auto *metaSignalType = PySideMetaSignal_TypeF(); + if (InitSignatureStrings(metaSignalType, MetaSignal_SignatureStrings) < 0) return; - Py_INCREF(PySideMetaSignal_TypeF()); - auto *obMetaSignal_Type = reinterpret_cast(PySideMetaSignal_TypeF()); - PyModule_AddObject(module, "MetaSignal", obMetaSignal_Type); + auto *obMetaSignalType = reinterpret_cast(metaSignalType); + Py_INCREF(obMetaSignalType); + PepModule_AddType(module, metaSignalType); - if (InitSignatureStrings(PySideSignal_TypeF(), Signal_SignatureStrings) < 0) + auto *signalType = PySideSignal_TypeF(); + if (InitSignatureStrings(signalType, Signal_SignatureStrings) < 0) return; - Py_INCREF(PySideSignal_TypeF()); - auto *obSignal_Type = reinterpret_cast(PySideSignal_TypeF()); - PyModule_AddObject(module, "Signal", obSignal_Type); + auto *obSignalType = reinterpret_cast(signalType); + Py_INCREF(obSignalType); + PepModule_AddType(module, signalType); - if (InitSignatureStrings(PySideSignalInstance_TypeF(), SignalInstance_SignatureStrings) < 0) + auto *signalInstanceType = PySideSignalInstance_TypeF(); + if (InitSignatureStrings(signalInstanceType, SignalInstance_SignatureStrings) < 0) return; - Py_INCREF(PySideSignalInstance_TypeF()); - auto *obSignalInstance_Type = reinterpret_cast(PySideSignalInstance_TypeF()); - PyModule_AddObject(module, "SignalInstance", obSignalInstance_Type); + auto *obSignalInstanceType = reinterpret_cast(signalInstanceType); + Py_INCREF(obSignalInstanceType); + PepModule_AddType(module, signalInstanceType); } bool checkType(PyObject *pyObj) @@ -988,23 +990,6 @@ void updateSourceObject(PyObject *source) return; } -// PYSIDE-2840: For an enum registered in Qt, return the C++ name. -// Ignore flags here; their underlying enums are of Python type flags anyways. -static QByteArray getQtEnumTypeName(PyTypeObject *type) -{ - if (!Shiboken::Enum::checkType(type)) - return {}; - - Shiboken::AutoDecRef qualName(PyObject_GetAttr(reinterpret_cast(type), - Shiboken::PyMagicName::qualname())); - QByteArray result = Shiboken::String::toCString(qualName.object()); - result.replace(".", "::"); - - const auto metaType = QMetaType::fromName(result); - return metaType.isValid() && metaType.flags().testFlag(QMetaType::IsEnumeration) - ? result : QByteArray{}; -} - QByteArray getTypeName(PyObject *obType) { if (PyType_Check(obType)) { @@ -1024,7 +1009,7 @@ QByteArray getTypeName(PyObject *obType) return QByteArrayLiteral("QVariantList"); if (type == &PyDict_Type) return QByteArrayLiteral("QVariantMap"); - QByteArray enumName = getQtEnumTypeName(type); + QByteArray enumName = PySide::QEnum::getTypeName(type); return enumName.isEmpty() ? "PyObject"_ba : enumName; } if (obType == Py_None) // Must be checked before as Shiboken::String::check accepts Py_None @@ -1278,7 +1263,7 @@ QByteArray getCallbackSignature(QMetaMethod signal, QObject *receiver, prefix += '('; for (int i = 0; i < mo->methodCount(); i++) { QMetaMethod me = mo->method(i); - if ((strncmp(me.methodSignature(), prefix, prefix.size()) == 0) && + if ((std::strncmp(me.methodSignature(), prefix, prefix.size()) == 0) && QMetaObject::checkConnectArgs(signal, me.methodSignature())) { numArgs = me.parameterTypes().size() + useSelf; break; @@ -1298,7 +1283,7 @@ QByteArray getCallbackSignature(QMetaMethod signal, QObject *receiver, prefix += '('; for (int i = 0, count = mo->methodCount(); i < count; ++i) { QMetaMethod me = mo->method(i); - if ((strncmp(me.methodSignature(), prefix, prefix.size()) == 0) && + if ((std::strncmp(me.methodSignature(), prefix, prefix.size()) == 0) && QMetaObject::checkConnectArgs(signal, me)) { numArgs = me.parameterTypes().size() + useSelf; break; diff --git a/sources/pyside6/libpyside/pysideslot.cpp b/sources/pyside6/libpyside/pysideslot.cpp index bc034c72..beb8a670 100644 --- a/sources/pyside6/libpyside/pysideslot.cpp +++ b/sources/pyside6/libpyside/pysideslot.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -171,17 +172,19 @@ DataList *dataListFromCapsule(PyObject *capsule) } static const char *Slot_SignatureStrings[] = { - "PySide6.QtCore.Slot(self,*types:type,name:str=nullptr,result:type=nullptr)", + "PySide6.QtCore.Slot(self,*types:typing.Union[type,str],name:str=nullptr,result:typing.Union[type,str]=nullptr)", "PySide6.QtCore.Slot.__call__(self,function:collections.abc.Callable[...,typing.Any])->typing.Any", nullptr}; // Sentinel void init(PyObject *module) { - if (InitSignatureStrings(PySideSlot_TypeF(), Slot_SignatureStrings) < 0) + auto *slotType = PySideSlot_TypeF(); + if (InitSignatureStrings(slotType, Slot_SignatureStrings) < 0) return; - Py_INCREF(PySideSlot_TypeF()); - PyModule_AddObject(module, "Slot", reinterpret_cast(PySideSlot_TypeF())); + auto *obSlotType = reinterpret_cast(slotType); + Py_INCREF(obSlotType); + PepModule_AddType(module, slotType); } } // namespace PySide::Slot diff --git a/sources/pyside6/libpyside/pysideutils.h b/sources/pyside6/libpyside/pysideutils.h index 579e7f74..db2f705e 100644 --- a/sources/pyside6/libpyside/pysideutils.h +++ b/sources/pyside6/libpyside/pysideutils.h @@ -41,9 +41,9 @@ PYSIDE_API bool isCompiledMethod(PyObject *callback); struct debugPyTypeObject { - PYSIDE_API explicit debugPyTypeObject(const PyTypeObject *o) noexcept; + PYSIDE_API explicit debugPyTypeObject(PyTypeObject *o) noexcept; - const PyTypeObject *m_object; + PyTypeObject *m_object; }; PYSIDE_API QDebug operator<<(QDebug debug, const debugPyTypeObject &o); @@ -57,6 +57,7 @@ struct debugPyObject PYSIDE_API QDebug operator<<(QDebug debug, const debugPyObject &o); +#if !defined(Py_LIMITED_API) || Py_LIMITED_API >= 0x030B0000 struct debugPyBuffer { PYSIDE_API explicit debugPyBuffer(Py_buffer *b) noexcept; @@ -65,6 +66,7 @@ struct debugPyBuffer }; PYSIDE_API QDebug operator<<(QDebug debug, const debugPyBuffer &b); +#endif // !Py_LIMITED_API || >= 3.11 } //namespace PySide diff --git a/sources/pyside6/libpyside/pysidevariantutils.cpp b/sources/pyside6/libpyside/pysidevariantutils.cpp new file mode 100644 index 00000000..5aaa363f --- /dev/null +++ b/sources/pyside6/libpyside/pysidevariantutils.cpp @@ -0,0 +1,219 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "pysidevariantutils.h" +#include "pysideutils.h" + +#include + +#include +#include +#include +#include + +using namespace Qt::StringLiterals; + +static const char qVariantTypeName[] = "QVariant"; + +static void warnConverter(const char *name) +{ + qWarning("Type converter for: %s not registered.", name); +} + +// Helper converting each item of a non-empty list using the "QVariant" converter +static std::optional pyListToVariantListHelper(PyObject *list, Py_ssize_t size) +{ + Q_ASSERT(size > 0); + QVariantList result; + result.reserve(size); + Shiboken::Conversions::SpecificConverter converter(qVariantTypeName); + if (!converter) { + warnConverter(qVariantTypeName); + return std::nullopt; + } + for (Py_ssize_t i = 0; i < size; ++i) { + Shiboken::AutoDecRef pyItem(PySequence_GetItem(list, i)); + QVariant item; + converter.toCpp(pyItem.object(), &item); + result.append(item); + } + return result; +} + +// Helper checking for a sequence of Unicode objects +static bool isStringList(PyObject *list) +{ + const Py_ssize_t size = PySequence_Size(list); + if (size == 0) + return false; + for (Py_ssize_t i = 0; i < size; ++i) { + Shiboken::AutoDecRef item(PySequence_GetItem(list, i)); + if (PyUnicode_Check(item) == 0) + return false; + } + return true; +} + +// Helper to convert to a QStringList +static std::optional listToStringList(PyObject *list) +{ + static const char listType[] = "QList"; + Shiboken::Conversions::SpecificConverter converter(listType); + if (!converter) { + warnConverter(listType); + return std::nullopt; + } + QStringList result; + converter.toCpp(list, &result); + return result; +} + +// Helper to convert a non-empty, homogenous list using the converter of the first item +static QVariant convertToValueList(PyObject *list) +{ + Q_ASSERT(PySequence_Size(list) >= 0); + + Shiboken::AutoDecRef element(PySequence_GetItem(list, 0)); + + auto *type = reinterpret_cast(element.object()); + QMetaType metaType = PySide::Variant::resolveMetaType(type); + if (!metaType.isValid()) + return {}; + + const QByteArray listTypeName = QByteArrayLiteral("QList<") + metaType.name() + '>'; + metaType = QMetaType::fromName(listTypeName); + if (!metaType.isValid()) + return {}; + + Shiboken::Conversions::SpecificConverter converter(listTypeName); + if (!converter) { + warnConverter(listTypeName.constData()); + return {}; + } + + QVariant var(metaType); + converter.toCpp(list, &var); + return var; +} + +namespace PySide::Variant +{ + +QMetaType resolveMetaType(PyTypeObject *type) +{ + if (!PyObject_TypeCheck(reinterpret_cast(type), SbkObjectType_TypeF())) + return {}; + const char *typeName = Shiboken::ObjectType::getOriginalName(type); + if (!typeName) + return {}; + const bool valueType = '*' != typeName[qstrlen(typeName) - 1]; + // Do not convert user type of value + if (valueType && Shiboken::ObjectType::isUserType(type)) + return {}; + QMetaType metaType = QMetaType::fromName(typeName); + if (metaType.isValid()) + return metaType; + // Do not resolve types to value type + if (valueType) + return {}; + // Find in base types. First check tp_bases, and only after check tp_base, because + // tp_base does not always point to the first base class, but rather to the first + // that has added any python fields or slots to its object layout. + // See https://mail.python.org/pipermail/python-list/2009-January/520733.html + if (type->tp_bases) { + const auto size = PyTuple_Size(type->tp_bases); + Py_ssize_t i = 0; + // PYSIDE-1887, PYSIDE-86: Skip QObject base class of QGraphicsObject; + // it needs to use always QGraphicsItem as a QVariant type for + // QGraphicsItem::itemChange() to work. + if (qstrcmp(typeName, "QGraphicsObject*") == 0 && size > 1) { + auto *firstBaseType = reinterpret_cast(PyTuple_GetItem(type->tp_bases, 0)); + if (SbkObjectType_Check(firstBaseType)) { + const char *firstBaseTypeName = Shiboken::ObjectType::getOriginalName(firstBaseType); + if (firstBaseTypeName != nullptr && qstrcmp(firstBaseTypeName, "QObject*") == 0) + ++i; + } + } + for ( ; i < size; ++i) { + auto baseType = reinterpret_cast(PyTuple_GetItem(type->tp_bases, i)); + const QMetaType derived = resolveMetaType(baseType); + if (derived.isValid()) + return derived; + } + return {}; + } + if (type->tp_base != nullptr) + return resolveMetaType(type->tp_base); + return {}; +} + +std::optional pyListToVariantList(PyObject *list) +{ + if (list == nullptr || PySequence_Check(list) == 0) + return std::nullopt; + const auto size = PySequence_Size(list); + if (size < 0) { // Some infinite (I/O read) thing? - bail out + PyErr_Clear(); + return std::nullopt; + } + if (size == 0) + return QVariantList{}; + return pyListToVariantListHelper(list, size); +} + +QVariant convertToVariantList(PyObject *list) +{ + const auto size = PySequence_Size(list); + if (size < 0) { // Some infinite (I/O read) thing? - bail out + PyErr_Clear(); + return {}; + } + if (size == 0) + return QVariantList{}; + + if (isStringList(list)) { + auto stringListO = listToStringList(list); + if (stringListO.has_value()) + return {stringListO.value()}; + } + + if (QVariant valueList = convertToValueList(list); valueList.isValid()) + return valueList; + + if (auto vlO = pyListToVariantListHelper(list, size); vlO.has_value()) + return vlO.value(); + + return {}; +} + +QVariant convertToVariantMap(PyObject *map) +{ + if (map == nullptr || PyDict_Check(map) == 0) + return {}; + + QVariantMap result; + if (PyDict_Size(map) == 0) + return result; + + Py_ssize_t pos = 0; + Shiboken::AutoDecRef keys(PyDict_Keys(map)); + if (!isStringList(keys)) + return {}; + + Shiboken::Conversions::SpecificConverter converter(qVariantTypeName); + if (!converter) { + warnConverter(qVariantTypeName); + return {}; + } + + PyObject *key{}; + PyObject *value{}; + while (PyDict_Next(map, &pos, &key, &value)) { + QVariant cppValue; + converter.toCpp(value, &cppValue); + result.insert(PySide::pyUnicodeToQString(key), cppValue); + } + return result; +} + +} // namespace PySide::Variant diff --git a/sources/pyside6/libpyside/pysidevariantutils.h b/sources/pyside6/libpyside/pysidevariantutils.h new file mode 100644 index 00000000..b53f7ce8 --- /dev/null +++ b/sources/pyside6/libpyside/pysidevariantutils.h @@ -0,0 +1,37 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef PYSIDEVARIANTUTILS_H +#define PYSIDEVARIANTUTILS_H + +#include + +#include + +#include +#include + +#include + +namespace PySide::Variant +{ + +/// Return a QMetaType for a PyTypeObject for purposes of +/// converting to a QVariant. +PYSIDE_API QMetaType resolveMetaType(PyTypeObject *type); + +/// Convert a heterogenous Python list to a QVariantList by converting each +/// item using the QVariant converter. +PYSIDE_API std::optional pyListToVariantList(PyObject *list); + +/// Converts a list to a QVariant following the PySide semantics: +/// - A list of strings is returned as QVariant +/// - A list of convertible values is returned as QVariant> +/// - Remaining types are returned as QVariant(QVariantList) +PYSIDE_API QVariant convertToVariantList(PyObject *list); + +/// Converts a map to a QVariantMap (string keys and QVariant values) +PYSIDE_API QVariant convertToVariantMap(PyObject *map); +} // namespace PySide::Variant + +#endif // PYSIDEVARIANTUTILS_H diff --git a/sources/pyside6/libpyside/qobjectconnect.cpp b/sources/pyside6/libpyside/qobjectconnect.cpp index 3c862b3a..d953c684 100644 --- a/sources/pyside6/libpyside/qobjectconnect.cpp +++ b/sources/pyside6/libpyside/qobjectconnect.cpp @@ -10,6 +10,7 @@ #include "signalmanager.h" #include +#include #include #include "basewrapper.h" #include "autodecref.h" @@ -211,8 +212,10 @@ QMetaObject::Connection qobjectConnectCallback(QObject *source, const char *sign } QMetaObject::Connection connection{}; + const bool connectByIndex = !receiver.forceDynamicSlot + && receiver.receiver != nullptr && receiver.slotIndex != -1; Py_BEGIN_ALLOW_THREADS // PYSIDE-2367, prevent threading deadlocks with connectNotify() - if (!receiver.forceDynamicSlot && receiver.receiver != nullptr && receiver.slotIndex != -1) { + if (connectByIndex) { connection = QMetaObject::connect(source, signalIndex, receiver.receiver, receiver.slotIndex, type); } else { @@ -233,7 +236,8 @@ QMetaObject::Connection qobjectConnectCallback(QObject *source, const char *sign if (!connection) return {}; - registerSlotConnection(source, signalIndex, callback, connection); + if (!connectByIndex) + registerSlotConnection(source, signalIndex, callback, connection); static_cast(source)->connectNotify(signalMethod); return connection; diff --git a/sources/pyside6/libpyside/signalmanager.cpp b/sources/pyside6/libpyside/signalmanager.cpp index a7bbcdf4..d8fd4118 100644 --- a/sources/pyside6/libpyside/signalmanager.cpp +++ b/sources/pyside6/libpyside/signalmanager.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -37,7 +38,11 @@ using namespace Qt::StringLiterals; #error QSLOT_CODE and/or QSIGNAL_CODE changed! change the hardcoded stuff to the correct value! #endif -static PyObject *metaObjectAttr = nullptr; +PyObject *metaObjectAttr() +{ + static PyObject *const s = Shiboken::String::createStaticString("__METAOBJECT__"); + return s; +} static int pyObjectWrapperMetaTypeId = QMetaType::UnknownType; @@ -251,7 +256,7 @@ PYSIDE_API QDebug operator<<(QDebug debug, const PyObjectWrapper &myObj) debug << '<'; if (PyObject *ob = myObj) { const auto refs = Py_REFCNT(ob); - debug << Py_TYPE(ob)->tp_name << " at " << ob; + debug << PepType_GetFullyQualifiedNameStr(Py_TYPE(ob)) << " at " << ob; if (refs == UINT_MAX) // _Py_IMMORTAL_REFCNT debug << ", immortal"; else @@ -308,9 +313,6 @@ void SignalManager::init() Shiboken::Conversions::registerConverterName(converter, "object"); Shiboken::Conversions::registerConverterName(converter, "PyObjectWrapper"); Shiboken::Conversions::registerConverterName(converter, "PySide::PyObjectWrapper"); - - if (!metaObjectAttr) - metaObjectAttr = Shiboken::String::fromCString("__METAOBJECT__"); } void SignalManager::setQmlMetaCallErrorHandler(QmlMetaCallErrorHandler handler) @@ -352,6 +354,18 @@ void SignalManager::handleMetaCallError() Py_SetRecursionLimit(reclimit); } +const char *metaObjectCallName(QMetaObject::Call call) +{ + static const char *names[] = { + "InvokeMetaMethod", "ReadProperty", "WriteProperty", "ResetProperty", + "CreateInstance", "IndexOfMethod", "RegisterPropertyMetaType", + "RegisterMethodArgumentMetaType", "BindableProperty", "CustomCall", + "ConstructInPlace"}; + constexpr size_t count = sizeof(names)/sizeof(names[0]); + static_assert(QMetaObject::ConstructInPlace == count - 1); + return call >= 0 && call < count ? names[call] : ""; +} + // Handler for QMetaObject::ReadProperty/WriteProperty/ResetProperty: int SignalManagerPrivate::qtPropertyMetacall(QObject *object, QMetaObject::Call call, @@ -384,25 +398,20 @@ int SignalManagerPrivate::qtPropertyMetacall(QObject *object, if (PyErr_Occurred()) { // PYSIDE-2160: An unknown type was reported. Indicated by StopIteration. if (PyErr_ExceptionMatches(PyExc_StopIteration)) { - PyObject *excType{}; - PyObject *excValue{}; - PyObject *excTraceback{}; - PyErr_Fetch(&excType, &excValue, &excTraceback); + Shiboken::Errors::Stash errorStash; bool ign = call == QMetaObject::WriteProperty; PyErr_WarnFormat(PyExc_RuntimeWarning, 0, ign ? "Unknown property type '%s' of QObject '%s' used in fset" : "Unknown property type '%s' of QObject '%s' used in fget with %R", - pp->d->typeName.constData(), metaObject->className(), excValue); + pp->d->typeName.constData(), metaObject->className(), errorStash.getException()); if (PyErr_Occurred()) Shiboken::Errors::storeErrorOrPrint(); - Py_DECREF(excType); - Py_DECREF(excValue); - Py_XDECREF(excTraceback); + errorStash.release(); return result; } qWarning().noquote().nospace() - << "An error occurred executing the property metacall " << call + << "An error occurred executing the property metacall " << metaObjectCallName(call) << " on property \"" << mp.name() << "\" of " << object; handleMetaCallError(object, &result); } @@ -519,7 +528,7 @@ static int callPythonMetaMethodHelper(const QByteArrayList ¶mTypes, } QScopedPointer retConverter; - if (isNonVoidReturn(returnType)) { + if (args[0] != nullptr && isNonVoidReturn(returnType)) { retConverter.reset(new Shiboken::Conversions::SpecificConverter(returnType)); if (!retConverter->isValid()) return CallResult::CallReturnValueError; @@ -612,13 +621,13 @@ static MetaObjectBuilder *metaBuilderFromDict(PyObject *dict) // no GIL. // Note that "SignalManager::registerMetaMethodGetIndex" has write actions // that might involve the interpreter, but in that context the GIL is held. - if (!dict || !PyDict_Contains(dict, metaObjectAttr)) + if (!dict || !PyDict_Contains(dict, metaObjectAttr())) return nullptr; // PYSIDE-813: The above assumption is not true in debug mode: // PyDict_GetItem would touch PyThreadState_GET and the global error state. // PyDict_GetItemWithError instead can work without GIL. - PyObject *pyBuilder = PyDict_GetItemWithError(dict, metaObjectAttr); + PyObject *pyBuilder = PyDict_GetItemWithError(dict, metaObjectAttr()); return reinterpret_cast(PyCapsule_GetPointer(pyBuilder, nullptr)); } @@ -692,7 +701,7 @@ static int addMetaMethod(QObject *source, const QByteArray &signature, if (dmo == nullptr) { dmo = new MetaObjectBuilder(Py_TYPE(pySelf), metaObject); PyObject *pyDmo = PyCapsule_New(dmo, nullptr, destroyMetaObject); - PyObject_SetAttr(pySelf, metaObjectAttr, pyDmo); + PyObject_SetAttr(pySelf, metaObjectAttr(), pyDmo); Py_DECREF(pyDmo); } @@ -742,6 +751,12 @@ int SignalManager::registerMetaMethodGetIndexBA(QObject* source, const QByteArra const QMetaObject *SignalManager::retrieveMetaObject(PyObject *self) { +#ifdef Py_GIL_DISABLED + // PYSIDE-2221: When working with disable-gil, it seems to be necessary + // to hold the GIL. Maybe that is harmless here (check later). + // Thanks to Sam Gross who fixed most errors by pointing this out. + Shiboken::GilState gil; +#endif // PYSIDE-803: Avoid the GIL in SignalManager::retrieveMetaObject // This function had the GIL. We do not use the GIL unless we have to. // metaBuilderFromDict accesses a Python dict, but in that context there diff --git a/sources/pyside6/libpysideqml/CMakeLists.txt b/sources/pyside6/libpysideqml/CMakeLists.txt index 77a405fb..1430334c 100644 --- a/sources/pyside6/libpysideqml/CMakeLists.txt +++ b/sources/pyside6/libpysideqml/CMakeLists.txt @@ -45,7 +45,6 @@ add_library(PySide6::pyside6qml ALIAS pyside6qml) target_include_directories(pyside6qml PUBLIC $ - $ ) target_compile_definitions(pyside6qml PRIVATE -DQT_LEAN_HEADERS=1 -DQT_NO_KEYWORDS=1) @@ -84,34 +83,21 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D QT_NO_CAST_FROM_ASCII -D QT_NO_CAST_T qfp_strip_library("pyside6qml") -# Install-tree / relocatable package config file. -configure_package_config_file( - "${CMAKE_CURRENT_SOURCE_DIR}/PySide6QmlConfig-spec.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/install/PySide6QmlConfig${SHIBOKEN_PYTHON_CONFIG_SUFFIX}.cmake" - INSTALL_DESTINATION "${LIB_INSTALL_DIR}/cmake/PySide6Qml" -) - -configure_file("${CMAKE_CURRENT_SOURCE_DIR}/PySide6QmlConfig.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/PySide6QmlConfig.cmake" @ONLY) -configure_file("${CMAKE_CURRENT_SOURCE_DIR}/PySide6QmlConfigVersion.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/PySide6QmlConfigVersion.cmake" @ONLY) - install(FILES ${libpysideqml_HEADERS} - DESTINATION include/${BINDING_NAME}${pyside6qml_SUFFIX}) + DESTINATION ${BINDING_NAME}${pyside6qml_SUFFIX}/include) -install(TARGETS pyside6qml EXPORT PySide6QmlTargets +#built-time installation +install(TARGETS pyside6qml EXPORT PySide6Targets LIBRARY DESTINATION "${LIB_INSTALL_DIR}" ARCHIVE DESTINATION "${LIB_INSTALL_DIR}" RUNTIME DESTINATION bin) -install(EXPORT PySide6QmlTargets NAMESPACE PySide6Qml:: - DESTINATION "${LIB_INSTALL_DIR}/cmake/PySide6Qml") -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/PySide6QmlConfig.cmake" - DESTINATION "${LIB_INSTALL_DIR}/cmake/PySide6Qml") - -install(FILES - "${CMAKE_CURRENT_BINARY_DIR}/install/PySide6QmlConfig${SHIBOKEN_PYTHON_CONFIG_SUFFIX}.cmake" - DESTINATION "${LIB_INSTALL_DIR}/cmake/PySide6Qml") - -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/PySide6QmlConfigVersion.cmake" - DESTINATION "${LIB_INSTALL_DIR}/cmake/PySide6Qml") +# install-tree or relocatable package installation +if(NOT is_pyside6_superproject_build) + set_target_properties(pyside6qml PROPERTIES + VERSION ${PYSIDE_SO_VERSION}) + install(TARGETS pyside6qml EXPORT PySide6WheelTargets + LIBRARY DESTINATION "PySide6" + ARCHIVE DESTINATION "PySide6" + RUNTIME DESTINATION "PySide6") +endif() diff --git a/sources/pyside6/libpysideqml/PySide6QmlConfig-spec.cmake.in b/sources/pyside6/libpysideqml/PySide6QmlConfig-spec.cmake.in deleted file mode 100644 index 36eb4123..00000000 --- a/sources/pyside6/libpysideqml/PySide6QmlConfig-spec.cmake.in +++ /dev/null @@ -1,7 +0,0 @@ -@PACKAGE_INIT@ - -# Import targets only when using an installed PySide6 config file (so not during a regular -# PySide6 build, or during a super project build). -if (NOT TARGET PySide6::pyside6qml) - include("${CMAKE_CURRENT_LIST_DIR}/PySide6QmlTargets.cmake") -endif() diff --git a/sources/pyside6/libpysideqml/PySide6QmlConfig.cmake.in b/sources/pyside6/libpysideqml/PySide6QmlConfig.cmake.in deleted file mode 100644 index dab0a6b1..00000000 --- a/sources/pyside6/libpysideqml/PySide6QmlConfig.cmake.in +++ /dev/null @@ -1,5 +0,0 @@ -if (NOT PYTHON_CONFIG_SUFFIX) - message(STATUS "PySide6QmlConfig: Using default python: @SHIBOKEN_PYTHON_CONFIG_SUFFIX@") - SET(PYTHON_CONFIG_SUFFIX @SHIBOKEN_PYTHON_CONFIG_SUFFIX@) -endif() -include(${CMAKE_CURRENT_LIST_DIR}/PySide6QmlConfig${PYTHON_CONFIG_SUFFIX}.cmake) diff --git a/sources/pyside6/libpysideqml/PySide6QmlConfigVersion.cmake.in b/sources/pyside6/libpysideqml/PySide6QmlConfigVersion.cmake.in deleted file mode 100644 index f5073ce0..00000000 --- a/sources/pyside6/libpysideqml/PySide6QmlConfigVersion.cmake.in +++ /dev/null @@ -1,10 +0,0 @@ -set(PACKAGE_VERSION @BINDING_API_VERSION@) - -if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}" ) - set(PACKAGE_VERSION_COMPATIBLE FALSE) -else("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}" ) - set(PACKAGE_VERSION_COMPATIBLE TRUE) - if( "${PACKAGE_FIND_VERSION}" STREQUAL "${PACKAGE_VERSION}") - set(PACKAGE_VERSION_EXACT TRUE) - endif( "${PACKAGE_FIND_VERSION}" STREQUAL "${PACKAGE_VERSION}") -endif("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}" ) diff --git a/sources/pyside6/libpysideqml/pysideqmlattached.cpp b/sources/pyside6/libpysideqml/pysideqmlattached.cpp index 41d7dee9..e4e9c16d 100644 --- a/sources/pyside6/libpysideqml/pysideqmlattached.cpp +++ b/sources/pyside6/libpysideqml/pysideqmlattached.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -59,7 +60,7 @@ static PyTypeObject *createPySideQmlAttachedType() PySide::ClassDecorator::Methods::typeSlots(); PyType_Spec PySideQmlAttachedType_spec = { - "2:PySide6.QtCore.qmlAttached", + "2:PySide6.QtCore.QmlAttached", sizeof(PySideClassDecorator), 0, Py_TPFLAGS_DEFAULT, @@ -107,7 +108,7 @@ static QObject *attachedFactoryHelper(PyTypeObject *attachingType, QObject *o) if (PyType_IsSubtype(pyResult->ob_type, qObjectType()) == 0) { qWarning("QmlAttached: Attached objects must inherit QObject, got %s.", - pyResult->ob_type->tp_name); + PepType_GetFullyQualifiedNameStr(Py_TYPE(pyResult))); return nullptr; } @@ -166,12 +167,13 @@ void initQmlAttached(PyObject *module) std::fill(attachingTypes, attachingTypes + MAX_ATTACHING_TYPES, nullptr); AttachedFactoryInitializer::init(); - if (InitSignatureStrings(PySideQmlAttached_TypeF(), qmlAttached_SignatureStrings) < 0) + auto *qmlAttachedType = PySideQmlAttached_TypeF(); + if (InitSignatureStrings(qmlAttachedType, qmlAttached_SignatureStrings) < 0) return; - Py_INCREF(PySideQmlAttached_TypeF()); - PyModule_AddObject(module, "QmlAttached", - reinterpret_cast(PySideQmlAttached_TypeF())); + auto *obQmlAttachedType = reinterpret_cast(qmlAttachedType); + Py_INCREF(obQmlAttachedType); + PepModule_AddType(module, qmlAttachedType); } PySide::Qml::QmlExtensionInfo qmlAttachedInfo(PyTypeObject *t, @@ -181,7 +183,7 @@ PySide::Qml::QmlExtensionInfo qmlAttachedInfo(PyTypeObject *t, if (!info || info->attachedType == nullptr) return result; - const auto *name = reinterpret_cast(t)->tp_name; + const auto *name = PepType_GetFullyQualifiedNameStr(reinterpret_cast(t)); if (nextAttachingType >= MAX_ATTACHING_TYPES) { qWarning("Unable to initialize attached type \"%s\": " "The limit %d of attached types has been reached.", @@ -208,7 +210,8 @@ QObject *qmlAttachedPropertiesObject(PyObject *typeObject, QObject *obj, bool cr auto *end = attachingTypes + nextAttachingType; auto *typePtr = std::find(attachingTypes, end, type); if (typePtr == end) { - qWarning("%s: Attaching type \"%s\" not found.", __FUNCTION__, type->tp_name); + qWarning("%s: Attaching type \"%s\" not found.", __FUNCTION__, + PepType_GetFullyQualifiedNameStr(type)); return nullptr; } diff --git a/sources/pyside6/libpysideqml/pysideqmlextended.cpp b/sources/pyside6/libpysideqml/pysideqmlextended.cpp index e2a96b60..f26fb9f8 100644 --- a/sources/pyside6/libpysideqml/pysideqmlextended.cpp +++ b/sources/pyside6/libpysideqml/pysideqmlextended.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -55,7 +56,7 @@ static PyTypeObject *createPySideQmlExtendedType() PySide::ClassDecorator::Methods::typeSlots(); PyType_Spec PySideQmlExtendedType_spec = { - "2:PySide6.QtCore.qmlExtended", + "2:PySide6.QtCore.QmlExtended", sizeof(PySideClassDecorator), 0, Py_TPFLAGS_DEFAULT, @@ -95,7 +96,8 @@ static QObject *extensionFactory(QObject *o) auto *pyObjType = Py_TYPE(pyObj); const auto info = qmlTypeInfo(reinterpret_cast(pyObjType)); if (!info || info->extensionType == nullptr) { - qWarning("QmlExtended: Cannot find extension of %s.", pyObjType->tp_name); + qWarning("QmlExtended: Cannot find extension of %s.", + PepType_GetFullyQualifiedNameStr(pyObjType)); return nullptr; } @@ -110,7 +112,7 @@ static QObject *extensionFactory(QObject *o) if (PyType_IsSubtype(pyResult->ob_type, qObjectType()) == 0) { qWarning("QmlExtended: Extension objects must inherit QObject, got %s.", - pyResult->ob_type->tp_name); + PepType_GetFullyQualifiedNameStr(pyResult->ob_type)); return nullptr; } @@ -121,12 +123,13 @@ static QObject *extensionFactory(QObject *o) void initQmlExtended(PyObject *module) { - if (InitSignatureStrings(PySideQmlExtended_TypeF(), qmlExtended_SignatureStrings) < 0) + auto *qmlExtendedType = PySideQmlExtended_TypeF(); + if (InitSignatureStrings(qmlExtendedType, qmlExtended_SignatureStrings) < 0) return; - Py_INCREF(PySideQmlExtended_TypeF()); - PyModule_AddObject(module, "QmlExtended", - reinterpret_cast(PySideQmlExtended_TypeF())); + auto *obQmlExtendedType = reinterpret_cast(qmlExtendedType); + Py_INCREF(obQmlExtendedType); + PepModule_AddType(module, qmlExtendedType); } PySide::Qml::QmlExtensionInfo qmlExtendedInfo(PyObject *t, @@ -139,7 +142,7 @@ PySide::Qml::QmlExtensionInfo qmlExtendedInfo(PyObject *t, result.factory = extensionFactory; } else { qWarning("Unable to retrieve meta object for %s", - reinterpret_cast(t)->tp_name); + PepType_GetFullyQualifiedNameStr(reinterpret_cast(t))); } } return result; diff --git a/sources/pyside6/libpysideqml/pysideqmlforeign.cpp b/sources/pyside6/libpysideqml/pysideqmlforeign.cpp index ef8d7fdf..e56eb1b2 100644 --- a/sources/pyside6/libpysideqml/pysideqmlforeign.cpp +++ b/sources/pyside6/libpysideqml/pysideqmlforeign.cpp @@ -57,7 +57,7 @@ static PyTypeObject *createPySideQmlForeignType() PySide::ClassDecorator::Methods::typeSlots(); PyType_Spec PySideQmlForeignType_spec = { - "2:PySide6.QtCore.qmlForeign", + "2:PySide6.QtCore.QmlForeign", sizeof(PySideClassDecorator), 0, Py_TPFLAGS_DEFAULT, @@ -83,12 +83,13 @@ namespace PySide::Qml { void initQmlForeign(PyObject *module) { - if (InitSignatureStrings(PySideQmlForeign_TypeF(), qmlForeign_SignatureStrings) < 0) + auto *foreignType = PySideQmlForeign_TypeF(); + if (InitSignatureStrings(foreignType, qmlForeign_SignatureStrings) < 0) return; - Py_INCREF(PySideQmlForeign_TypeF()); - PyModule_AddObject(module, "QmlForeign", - reinterpret_cast(PySideQmlForeign_TypeF())); + auto *obForeignType = reinterpret_cast(foreignType); + Py_INCREF(obForeignType); + PepModule_AddType(module, foreignType); } } // namespace PySide::Qml diff --git a/sources/pyside6/libpysideqml/pysideqmllistproperty.cpp b/sources/pyside6/libpysideqml/pysideqmllistproperty.cpp index 5011fd61..a48a3d4d 100644 --- a/sources/pyside6/libpysideqml/pysideqmllistproperty.cpp +++ b/sources/pyside6/libpysideqml/pysideqmllistproperty.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -306,9 +307,10 @@ void initQtQmlListProperty(PyObject *module) // Register QQmlListProperty metatype for use in QML qRegisterMetaType>(); - Py_INCREF(reinterpret_cast(PropertyList_TypeF())); - PyModule_AddObject(module, PepType_GetNameStr(PropertyList_TypeF()), - reinterpret_cast(PropertyList_TypeF())); + auto *propertyListType = PropertyList_TypeF(); + auto *obPropertyListType = reinterpret_cast(propertyListType); + Py_INCREF(obPropertyListType); + PepModule_AddType(module, propertyListType); } } // namespace PySide::Qml diff --git a/sources/pyside6/libpysideqml/pysideqmlmetacallerror.cpp b/sources/pyside6/libpysideqml/pysideqmlmetacallerror.cpp index a3d2664c..4e0afa3b 100644 --- a/sources/pyside6/libpysideqml/pysideqmlmetacallerror.cpp +++ b/sources/pyside6/libpysideqml/pysideqmlmetacallerror.cpp @@ -5,6 +5,7 @@ #include #include +#include #include // Remove deprecated MACRO of copysign for MSVC #86286 @@ -40,17 +41,17 @@ std::optional qmlMetaCallErrorHandler(QObject *object) if (engine->currentStackFrame == nullptr) return {}; - PyObject *errType{}; - PyObject *errValue{}; - PyObject *errTraceback{}; - PyErr_Fetch(&errType, &errValue, &errTraceback); + Shiboken::Errors::Stash errorStash; + PyObject *errValue = errorStash.getException(); // PYSIDE-464: The error is only valid before PyErr_Restore, // PYSIDE-464: therefore we take local copies. Shiboken::AutoDecRef objStr(PyObject_Str(errValue)); const QString errString = QString::fromUtf8(Shiboken::String::toCString(objStr)); - const bool isSyntaxError = errType == PyExc_SyntaxError; - const bool isTypeError = errType == PyExc_TypeError; - PyErr_Restore(errType, errValue, errTraceback); + const bool isSyntaxError = errValue != nullptr + && PyErr_GivenExceptionMatches(errValue, PyExc_SyntaxError); + const bool isTypeError = errValue != nullptr + && PyErr_GivenExceptionMatches(errValue, PyExc_TypeError); + errorStash.restore(); PyErr_Print(); // Note: PyErr_Print clears the error. diff --git a/sources/pyside6/libpysideqml/pysideqmlnamedelement.cpp b/sources/pyside6/libpysideqml/pysideqmlnamedelement.cpp index a0c05b38..3178f433 100644 --- a/sources/pyside6/libpysideqml/pysideqmlnamedelement.cpp +++ b/sources/pyside6/libpysideqml/pysideqmlnamedelement.cpp @@ -41,7 +41,7 @@ PyTypeObject *createPySideQmlNamedElementType(void) PySide::ClassDecorator::Methods::typeSlots(); PyType_Spec PySideQmlNamedElementType_spec = { - "2:PySide6.QtCore.qmlNamedElement", + "2:PySide6.QtCore.QmlNamedElement", sizeof(PySideClassDecorator), 0, Py_TPFLAGS_DEFAULT, @@ -65,10 +65,11 @@ static const char *qmlNamedElement_SignatureStrings[] = { void initQmlNamedElement(PyObject *module) { - if (InitSignatureStrings(PySideQmlNamedElement_TypeF(), qmlNamedElement_SignatureStrings) < 0) + auto *qmlNamedElementType = PySideQmlNamedElement_TypeF(); + if (InitSignatureStrings(qmlNamedElementType, qmlNamedElement_SignatureStrings) < 0) return; - Py_INCREF(PySideQmlNamedElement_TypeF()); - PyModule_AddObject(module, "QmlNamedElement", - reinterpret_cast(PySideQmlNamedElement_TypeF())); + auto *obQmlNamedElementType = reinterpret_cast(qmlNamedElementType); + Py_INCREF(obQmlNamedElementType); + PepModule_AddType(module, qmlNamedElementType); } diff --git a/sources/pyside6/libpysideqml/pysideqmlregistertype.cpp b/sources/pyside6/libpysideqml/pysideqmlregistertype.cpp index b64a72f8..d6548131 100644 --- a/sources/pyside6/libpysideqml/pysideqmlregistertype.cpp +++ b/sources/pyside6/libpysideqml/pysideqmlregistertype.cpp @@ -14,6 +14,7 @@ // shiboken #include #include +#include #include #include @@ -98,8 +99,11 @@ static inline bool isQmlParserStatus(const QMetaObject *o) static QByteArray getGlobalString(const char *name) { - PyObject *globalVar = PyDict_GetItemString(PyEval_GetGlobals(), name); + Shiboken::AutoDecRef globals(PepEval_GetFrameGlobals()); + if (globals.isNull()) + return {}; + PyObject *globalVar = PyDict_GetItemString(globals, name); if (globalVar == nullptr || PyUnicode_Check(globalVar) == 0) return {}; @@ -109,8 +113,11 @@ static QByteArray getGlobalString(const char *name) static int getGlobalInt(const char *name) { - PyObject *globalVar = PyDict_GetItemString(PyEval_GetGlobals(), name); + Shiboken::AutoDecRef globals(PepEval_GetFrameGlobals()); + if (globals.isNull()) + return -1; + PyObject *globalVar = PyDict_GetItemString(globals, name); if (globalVar == nullptr || PyLong_Check(globalVar) == 0) return -1; @@ -208,7 +215,7 @@ static int qmlRegisterType(PyObject *pyObj, // there's no way to unregister a QML type. Py_INCREF(pyObj); - const QByteArray typeName(pyObjType->tp_name); + const QByteArray typeName(PepType_GetFullyQualifiedNameStr(pyObjType)); QByteArray ptrType = typeName + '*'; QByteArray listType = QByteArrayLiteral("QQmlListProperty<") + typeName + '>'; const auto typeId = QMetaType(new QQmlMetaTypeInterface(ptrType)); @@ -651,8 +658,10 @@ static std::optional Shiboken::AutoDecRef tpDict(PepType_GetDict(pyObjType)); auto *create = PyDict_GetItemString(tpDict.object(), "create"); // Method decorated by "@staticmethod" - if (create == nullptr || std::strcmp(Py_TYPE(create)->tp_name, "staticmethod") != 0) + if (create == nullptr + || std::strcmp(PepType_GetFullyQualifiedNameStr(Py_TYPE(create)), "staticmethod") != 0) { return std::nullopt; + } // 3.10: "__wrapped__" Shiboken::AutoDecRef function(PyObject_GetAttrString(create, "__func__")); if (function.isNull()) { diff --git a/sources/pyside6/libpysideqml/pysideqmltypeinfo.cpp b/sources/pyside6/libpysideqml/pysideqmltypeinfo.cpp index d1d56efa..97ebf220 100644 --- a/sources/pyside6/libpysideqml/pysideqmltypeinfo.cpp +++ b/sources/pyside6/libpysideqml/pysideqmltypeinfo.cpp @@ -6,6 +6,8 @@ #include #include +#include + #include namespace PySide::Qml { @@ -43,11 +45,11 @@ QDebug operator<<(QDebug d, const QmlTypeInfo &i) d.nospace(); d << "QmlTypeInfo(" << i.flags; if (i.foreignType) - d << ", foreignType=" << i.foreignType->tp_name; + d << ", foreignType=" << PepType_GetFullyQualifiedNameStr(i.foreignType); if (i.attachedType) - d << ", attachedType=" << i.attachedType->tp_name; + d << ", attachedType=" << PepType_GetFullyQualifiedNameStr(i.attachedType); if (i.extensionType) - d << ", extensionType=" << i.extensionType->tp_name; + d << ", extensionType=" << PepType_GetFullyQualifiedNameStr(i.extensionType); d << ')'; return d; } diff --git a/sources/pyside6/libpysideqml/pysideqmluncreatable.cpp b/sources/pyside6/libpysideqml/pysideqmluncreatable.cpp index 348d53d5..2397fa61 100644 --- a/sources/pyside6/libpysideqml/pysideqmluncreatable.cpp +++ b/sources/pyside6/libpysideqml/pysideqmluncreatable.cpp @@ -72,7 +72,7 @@ PyTypeObject *createPySideQmlUncreatableType(void) PySide::ClassDecorator::Methods::typeSlots(); PyType_Spec PySideQmlUncreatableType_spec = { - "2:PySide6.QtCore.qmlUncreatable", + "2:PySide6.QtCore.QmlUncreatable", sizeof(PySideClassDecorator), 0, Py_TPFLAGS_DEFAULT, @@ -96,12 +96,13 @@ static const char *qmlUncreatable_SignatureStrings[] = { void initQmlUncreatable(PyObject *module) { - if (InitSignatureStrings(PySideQmlUncreatable_TypeF(), qmlUncreatable_SignatureStrings) < 0) + auto *qmlUncreatableType = PySideQmlUncreatable_TypeF(); + if (InitSignatureStrings(qmlUncreatableType, qmlUncreatable_SignatureStrings) < 0) return; - Py_INCREF(PySideQmlUncreatable_TypeF()); - PyModule_AddObject(module, "QmlUncreatable", - reinterpret_cast(PySideQmlUncreatable_TypeF())); + auto *obQmlUncreatableType = reinterpret_cast(qmlUncreatableType); + Py_INCREF(obQmlUncreatableType); + PepModule_AddType(module, qmlUncreatableType); } void setUncreatableClassInfo(PyTypeObject *type, const QByteArray &reason) diff --git a/sources/pyside6/libpysideremoteobjects/CMakeLists.txt b/sources/pyside6/libpysideremoteobjects/CMakeLists.txt index 4669fb67..caaadc24 100644 --- a/sources/pyside6/libpysideremoteobjects/CMakeLists.txt +++ b/sources/pyside6/libpysideremoteobjects/CMakeLists.txt @@ -80,7 +80,7 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D QT_NO_CAST_FROM_ASCII -D QT_NO_CAST_T # install(FILES ${libpysideremoteobjects_HEADERS} - DESTINATION include/${BINDING_NAME}${pyside6remoteobjects_SUFFIX}) + DESTINATION ${BINDING_NAME}${pyside6remoteobjects_SUFFIX}/include) install(TARGETS pyside6remoteobjects EXPORT PySide6RemoteObjectsTargets LIBRARY DESTINATION "${LIB_INSTALL_DIR}" diff --git a/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod.cpp b/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod.cpp index d5a5454f..aad440da 100644 --- a/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod.cpp +++ b/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod.cpp @@ -3,6 +3,9 @@ #include "pysidecapsulemethod_p.h" +#include +#include + extern "C" { diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamicclass.cpp b/sources/pyside6/libpysideremoteobjects/pysidedynamicclass.cpp index 941e38c6..8f685713 100644 --- a/sources/pyside6/libpysideremoteobjects/pysidedynamicclass.cpp +++ b/sources/pyside6/libpysideremoteobjects/pysidedynamicclass.cpp @@ -10,6 +10,7 @@ #include "pysiderephandler_p.h" #include +#include #include #include @@ -25,6 +26,9 @@ #include #include +#include +#include + using namespace Shiboken; class FriendlyReplica : public QRemoteObjectReplica @@ -167,7 +171,7 @@ struct SourceDefs auto name = callData->name.sliced(4); auto index = metaObject->indexOfProperty(name.constData()); if (index < 0) { - name[0] = tolower(name[0]); // Try lower case + name[0] = std::tolower(name[0]); // Try lower case index = metaObject->indexOfProperty(name.constData()); } // It is possible a .rep names a Slot "push" or "pushSomething" that @@ -232,7 +236,7 @@ struct ReplicaDefs PyObject *name = nullptr; static PyTypeObject *nodeType = Shiboken::Conversions::getPythonTypeObject("QRemoteObjectNode"); if (!PyArg_UnpackTuple(args, "Replica.__init__", 2, 3, &node, &constructorType, &name) || - !PySide::inherits(Py_TYPE(node), nodeType->tp_name)) { + !PySide::inherits(Py_TYPE(node), PepType_GetFullyQualifiedNameStr(nodeType))) { PyErr_SetString(PyExc_TypeError, "Replicas can be initialized with no arguments or by node.acquire only"); return -1; @@ -378,7 +382,7 @@ PyTypeObject *createDynamicClassImpl(QMetaObject *meta) auto fullTypeName = QByteArray{T::getTypePrefix()} + meta->className(); PyType_Spec spec = { - fullTypeName.constData(), + qstrdup(fullTypeName.constData()), 0, 0, Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, @@ -478,9 +482,9 @@ PyTypeObject *createDynamicClassImpl(QMetaObject *meta) PyTypeObject *createDynamicClass(QMetaObject *meta, PyObject *properties_capsule) { bool isSource; - if (strncmp(meta->superClass()->className(), "QObject", 7) == 0) { + if (std::strncmp(meta->superClass()->className(), "QObject", 7) == 0) { isSource = true; - } else if (strncmp(meta->superClass()->className(), "QRemoteObjectReplica", 20) == 0) { + } else if (std::strncmp(meta->superClass()->className(), "QRemoteObjectReplica", 20) == 0) { isSource = false; } else { PyErr_SetString(PyExc_RuntimeError, diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon.cpp b/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon.cpp index 1e8bc327..b1b67510 100644 --- a/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon.cpp +++ b/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon.cpp @@ -4,6 +4,7 @@ #include "pysidedynamiccommon_p.h" #include "pysidedynamicenum_p.h" +#include #include #include @@ -96,7 +97,7 @@ int create_managed_py_enums(PyObject *self, QMetaObject *meta) if (PyObject_SetAttrString(self, "_enum_data", enum_data) < 0) { PyErr_Print(); qWarning() << "Failed to set _enum_data attribute on type" - << reinterpret_cast(self)->tp_name; + << PepType_GetFullyQualifiedNameStr(reinterpret_cast(self)); return -1; } Py_DECREF(enum_data); diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamicpod.cpp b/sources/pyside6/libpysideremoteobjects/pysidedynamicpod.cpp index abfeaa03..5dfdb448 100644 --- a/sources/pyside6/libpysideremoteobjects/pysidedynamicpod.cpp +++ b/sources/pyside6/libpysideremoteobjects/pysidedynamicpod.cpp @@ -74,7 +74,7 @@ struct PodDefs static PyObject *tp_repr(PyObject *self) { auto *type = Py_TYPE(self); - std::string repr(type->tp_name); + std::string repr(PepType_GetFullyQualifiedNameStr(type)); repr += "("; for (Py_ssize_t i = 0; i < PyTuple_Size(self); ++i) { if (i > 0) @@ -203,8 +203,9 @@ PyTypeObject *createPodType(QMetaObject *meta) return nullptr; } auto *pyType = Conversions::getPythonTypeObject(metaType.name()); - Py_INCREF(pyType); - PyTuple_SetItem(pyParamTypes, i, reinterpret_cast(pyType)); + auto *obPyType = reinterpret_cast(pyType); + Py_INCREF(obPyType); + PyTuple_SetItem(pyParamTypes, i, obPyType); } auto *type = reinterpret_cast(obType); @@ -229,10 +230,8 @@ PyTypeObject *createPodType(QMetaObject *meta) PyCapsule_GetPointer(capsule, "PropertyCapsule")); }); auto *capsulePropObject = make_capsule_property(&method, capsule); - if (PyObject_SetAttrString(reinterpret_cast(type), metaProperty.name(), - capsulePropObject) < 0) { + if (PyObject_SetAttrString(obType, metaProperty.name(), capsulePropObject) < 0) return nullptr; - } Py_DECREF(capsulePropObject); } @@ -242,11 +241,12 @@ PyTypeObject *createPodType(QMetaObject *meta) // to the type's attributes. So we need to decrease the ref count on the type // after calling createConverter. auto *converter = Shiboken::Conversions::createConverter(type, cppToPython_POD_Tuple); - Py_DECREF(type); + Py_DECREF(obType); if (set_cleanup_capsule_attr_for_pointer(type, "_converter_capsule", converter) < 0) return nullptr; Shiboken::Conversions::registerConverterName(converter, meta->className()); - Shiboken::Conversions::registerConverterName(converter, type->tp_name); + Shiboken::Conversions::registerConverterName(converter, + PepType_GetFullyQualifiedNameStr(type)); Shiboken::Conversions::addPythonToCppValueConversion(converter, pythonToCpp_Tuple_POD, is_Tuple_PythonToCpp_POD_Convertible); diff --git a/sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp b/sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp index bfe08545..5804add3 100644 --- a/sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp +++ b/sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp @@ -7,6 +7,7 @@ #include "pysidedynamiccommon_p.h" #include +#include #include #include #include @@ -367,22 +368,16 @@ bool instantiateFromDefaultValue(QVariant &variant, const QString &defaultValue) static PyObject *pyLocals = PyDict_New(); // Create the Python expression to evaluate - std::string code = std::string(pyType->tp_name) + '(' + std::string code = std::string(PepType_GetFullyQualifiedNameStr(pyType)) + '(' + defaultValue.toUtf8().constData() + ')'; PyObject *pyResult = PyRun_String(code.c_str(), Py_eval_input, pyLocals, pyLocals); if (!pyResult) { - PyObject *ptype = nullptr; - PyObject *pvalue = nullptr; - PyObject *ptraceback = nullptr; - PyErr_Fetch(&ptype, &pvalue, &ptraceback); - PyErr_NormalizeException(&ptype, &pvalue, &ptraceback); + Shiboken::Errors::Stash errorStash; PyErr_Format(PyExc_TypeError, "Failed to generate default value. Error: %s. Problematic code: %s", - Shiboken::String::toCString(PyObject_Str(pvalue)), code.c_str()); - Py_XDECREF(ptype); - Py_XDECREF(pvalue); - Py_XDECREF(ptraceback); + Shiboken::String::toCString(PyObject_Str(errorStash.getException())), code.c_str()); + errorStash.release(); Py_DECREF(pyLocals); return false; } @@ -446,8 +441,10 @@ void init(PyObject *module) qRegisterMetaType(); qRegisterMetaType(); - Py_INCREF(PySideRepFile_TypeF()); - PyModule_AddObject(module, "RepFile", reinterpret_cast(PySideRepFile_TypeF())); + auto *repType = PySideRepFile_TypeF(); + auto *obRepType = reinterpret_cast(repType); + Py_INCREF(obRepType); + PepModule_AddType(module, repType); // Add a test helper to verify type reference counting static PyMethodDef get_capsule_count_def = { @@ -457,7 +454,7 @@ void init(PyObject *module) "Returns the current count of PyCapsule objects" // docstring }; - PyModule_AddObject(module, "getCapsuleCount", PyCFunction_New(&get_capsule_count_def, nullptr)); + PepModule_Add(module, "getCapsuleCount", PyCFunction_New(&get_capsule_count_def, nullptr)); } } // namespace PySide::RemoteObjects diff --git a/sources/pyside6/plugins/designer/CMakeLists.txt b/sources/pyside6/plugins/designer/CMakeLists.txt index b5a00dd4..f886c7f3 100644 --- a/sources/pyside6/plugins/designer/CMakeLists.txt +++ b/sources/pyside6/plugins/designer/CMakeLists.txt @@ -22,7 +22,10 @@ target_sources(PySidePlugin PRIVATE target_compile_definitions(PySidePlugin PRIVATE -DQT_NO_KEYWORDS=1) -if(PYTHON_LIMITED_API) +# For Windows we use the limited API by default +# See ShibokenHelpers.cmake::shiboken_check_if_limited_api() which is called always +# with default FORCE_LIMITED_API set to TRUE for building libshiboken +if(FORCE_LIMITED_API OR WIN32) target_compile_definitions(PySidePlugin PRIVATE "-DPy_LIMITED_API=0x03090000") endif() diff --git a/sources/pyside6/plugins/designer/designercustomwidgets.cpp b/sources/pyside6/plugins/designer/designercustomwidgets.cpp index 07faa5b0..d1353985 100644 --- a/sources/pyside6/plugins/designer/designercustomwidgets.cpp +++ b/sources/pyside6/plugins/designer/designercustomwidgets.cpp @@ -57,6 +57,7 @@ static QString pyStr(PyObject *o) static QString pyErrorMessage() { QString result = ""_L1; +#if (defined(Py_LIMITED_API) && Py_LIMITED_API < 0x030C0000) || (!defined(Py_LIMITED_API) && PY_VERSION_HEX < 0x030C0000) PyObject *ptype = {}; PyObject *pvalue = {}; PyObject *ptraceback = {}; @@ -64,6 +65,12 @@ static QString pyErrorMessage() if (pvalue != nullptr) result = pyStr(pvalue); PyErr_Restore(ptype, pvalue, ptraceback); +#else // <3.11 + if (PyObject *pvalue = PyErr_GetRaisedException()) { + result = pyStr(pvalue); + Py_DECREF(pvalue); + } +#endif return result; } diff --git a/sources/pyside6/plugins/uitools/CMakeLists.txt b/sources/pyside6/plugins/uitools/CMakeLists.txt index 06d0ae90..f111109f 100644 --- a/sources/pyside6/plugins/uitools/CMakeLists.txt +++ b/sources/pyside6/plugins/uitools/CMakeLists.txt @@ -18,7 +18,7 @@ add_library(uiplugin STATIC ${ui_plugin_src}) if(CMAKE_HOST_UNIX AND NOT CYGWIN) add_definitions(-fPIC) endif() -add_definitions(-DQT_STATICPLUGIN) +add_compile_definitions(QT_STATICPLUGIN) set_property(TARGET pyside6 PROPERTY CXX_STANDARD 17) diff --git a/sources/pyside6/plugins/uitools/customwidget.cpp b/sources/pyside6/plugins/uitools/customwidget.cpp index 19e285c3..361c02fa 100644 --- a/sources/pyside6/plugins/uitools/customwidget.cpp +++ b/sources/pyside6/plugins/uitools/customwidget.cpp @@ -9,12 +9,14 @@ #include #include #include +#include + // Part of the static plugin linked to the QtUiLoader Python module, // allowing it to create a custom widget written in Python. PyCustomWidget::PyCustomWidget(PyObject *objectType) : m_pyObject(objectType), - m_name(QString::fromUtf8(reinterpret_cast(objectType)->tp_name)) + m_name(QString::fromUtf8(PepType_GetFullyQualifiedNameStr(reinterpret_cast(objectType)))) { } @@ -88,20 +90,21 @@ QWidget *PyCustomWidget::createWidget(QWidget *parent) PyTuple_SetItem(pyArgs.object(), 0, pyParent); // tuple will keep pyParent reference // Call python constructor - auto *result = reinterpret_cast(PyObject_CallObject(m_pyObject, pyArgs)); - if (result == nullptr) { + auto *obResult = PyObject_CallObject(m_pyObject, pyArgs); + if (obResult == nullptr) { qWarning("Unable to create a Python custom widget of type \"%s\".", qPrintable(m_name)); PyErr_Print(); return nullptr; } + auto *result = reinterpret_cast(obResult); if (unknownParent) // if parent does not exist in python, transfer the ownership to cpp Shiboken::Object::releaseOwnership(result); else - Shiboken::Object::setParent(pyParent, reinterpret_cast(result)); + Shiboken::Object::setParent(pyParent, obResult); - return reinterpret_cast(Shiboken::Object::cppPointer(result, Py_TYPE(result))); + return reinterpret_cast(Shiboken::Object::cppPointer(result, Py_TYPE(obResult))); } void PyCustomWidget::initialize(QDesignerFormEditorInterface *) diff --git a/sources/pyside6/qtexampleicons/CMakeLists.txt b/sources/pyside6/qtexampleicons/CMakeLists.txt deleted file mode 100644 index c6446c4c..00000000 --- a/sources/pyside6/qtexampleicons/CMakeLists.txt +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (C) 2023 The Qt Company Ltd. -# SPDX-License-Identifier: BSD-3-Clause - -cmake_minimum_required(VERSION 3.18) -cmake_policy(VERSION 3.18) - -project(QtExampleIcons) - -set(CMAKE_INCLUDE_CURRENT_DIR ON) - -set(CMAKE_AUTORCC ON) - -set(CMAKE_AUTOMOC ON) - -find_package(Qt6 COMPONENTS ExampleIconsPrivate) - -add_library(QtExampleIcons MODULE module.c) - -# See libshiboken/CMakeLists.txt -if(PYTHON_LIMITED_API) - target_compile_definitions(QtExampleIcons PRIVATE "-DPy_LIMITED_API=0x03090000") -endif() - -if(CMAKE_BUILD_TYPE STREQUAL "Debug") - if(PYTHON_WITH_DEBUG) - target_compile_definitions(QtExampleIcons PRIVATE "-DPy_DEBUG") - endif() - if (PYTHON_WITH_COUNT_ALLOCS) - target_compile_definitions(QtExampleIcons PRIVATE "-DCOUNT_ALLOCS") - endif() -elseif(CMAKE_BUILD_TYPE STREQUAL "Release") - target_compile_definitions(QtExampleIcons PRIVATE "-DNDEBUG") -endif() - -target_include_directories(QtExampleIcons PRIVATE ${SHIBOKEN_PYTHON_INCLUDE_DIRS}) - -get_property(SHIBOKEN_PYTHON_LIBRARIES GLOBAL PROPERTY shiboken_python_libraries) - -target_link_libraries(QtExampleIcons PRIVATE - Qt::ExampleIconsPrivate - ${SHIBOKEN_PYTHON_LIBRARIES}) - -set_target_properties(QtExampleIcons PROPERTIES - PREFIX "" - OUTPUT_NAME "QtExampleIcons${SHIBOKEN_PYTHON_EXTENSION_SUFFIX}" - LIBRARY_OUTPUT_DIRECTORY "${pyside6_BINARY_DIR}") - -if(WIN32) - set_property(TARGET QtExampleIcons PROPERTY SUFFIX ".pyd") -endif() - -install(TARGETS QtExampleIcons LIBRARY DESTINATION "${PYTHON_SITE_PACKAGES}/PySide6") diff --git a/sources/pyside6/qtexampleicons/module.c b/sources/pyside6/qtexampleicons/module.c deleted file mode 100644 index b728dafe..00000000 --- a/sources/pyside6/qtexampleicons/module.c +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include - -#if defined _WIN32 -# define MODULE_EXPORT __declspec(dllexport) -#else -# define MODULE_EXPORT __attribute__ ((visibility("default"))) -#endif - -static PyMethodDef QtExampleIconsMethods[] = { - {NULL, NULL, 0, NULL} -}; - -static struct PyModuleDef moduleDef = { - /* m_base */ PyModuleDef_HEAD_INIT, - /* m_name */ "QtExampleIcons", - /* m_doc */ NULL, - /* m_size */ -1, - /* m_methods */ QtExampleIconsMethods, - /* m_reload */ NULL, - /* m_traverse */ NULL, - /* m_clear */ NULL, - /* m_free */ NULL -}; - -MODULE_EXPORT PyObject *PyInit_QtExampleIcons(void) -{ - return PyModule_Create(&moduleDef); -} - -int main(int argc, char *argv[]) -{ -#ifndef PYPY_VERSION - Py_Initialize(); -#endif - PyInit_QtExampleIcons(); - return 0; -} diff --git a/sources/pyside6/tests/CMakeLists.txt b/sources/pyside6/tests/CMakeLists.txt index 539e1aea..40df2c68 100644 --- a/sources/pyside6/tests/CMakeLists.txt +++ b/sources/pyside6/tests/CMakeLists.txt @@ -48,6 +48,10 @@ endmacro() if (NOT DISABLE_QtCore AND NOT DISABLE_QtGui AND NOT DISABLE_QtWidgets) add_subdirectory(pysidetest) endif() +if (NOT DISABLE_QtAsyncio) + add_subdirectory(QtAsyncio) +endif() + add_subdirectory(registry) add_subdirectory(signals) add_subdirectory(support) @@ -68,6 +72,6 @@ foreach(shortname IN LISTS all_module_shortnames) endforeach() #platform specific -if (ENABLE_MAC) +if (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") add_subdirectory(mac) endif () diff --git a/sources/pyside6/tests/QtAsyncio/CMakeLists.txt b/sources/pyside6/tests/QtAsyncio/CMakeLists.txt index 935e0d90..221b74b6 100644 --- a/sources/pyside6/tests/QtAsyncio/CMakeLists.txt +++ b/sources/pyside6/tests/QtAsyncio/CMakeLists.txt @@ -1,2 +1,11 @@ +PYSIDE_TEST(bug_2790.py) +PYSIDE_TEST(bug_2799.py) PYSIDE_TEST(qasyncio_test.py) +PYSIDE_TEST(qasyncio_test_cancel_task.py) +PYSIDE_TEST(qasyncio_test_cancel_taskgroup.py) PYSIDE_TEST(qasyncio_test_chain.py) +PYSIDE_TEST(qasyncio_test_executor.py) +PYSIDE_TEST(qasyncio_test_queues.py) +PYSIDE_TEST(qasyncio_test_threadsafe.py) +PYSIDE_TEST(qasyncio_test_time.py) +PYSIDE_TEST(qasyncio_test_uncancel.py) diff --git a/sources/pyside6/tests/QtAsyncio/bug_2790.py b/sources/pyside6/tests/QtAsyncio/bug_2790.py index 9fd152b1..5bf91931 100644 --- a/sources/pyside6/tests/QtAsyncio/bug_2790.py +++ b/sources/pyside6/tests/QtAsyncio/bug_2790.py @@ -4,9 +4,16 @@ from __future__ import annotations '''Test cases for QtAsyncio''' +import os +import sys import unittest -import asyncio +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +import asyncio import PySide6.QtAsyncio as QtAsyncio @@ -29,6 +36,7 @@ class QAsyncioTestCaseBug2790(unittest.TestCase): except TimeoutError: outputs.append("Timeout") + @unittest.skipIf(sys.version_info < (3, 11), "requires asyncio.timeout") def test_timeout(self): # The Qt event loop (and thus QtAsyncio) does not guarantee that events # will be processed in the order they were posted, so there is two diff --git a/sources/pyside6/tests/QtAsyncio/bug_2799.py b/sources/pyside6/tests/QtAsyncio/bug_2799.py index eb86c7d4..c277bd5b 100644 --- a/sources/pyside6/tests/QtAsyncio/bug_2799.py +++ b/sources/pyside6/tests/QtAsyncio/bug_2799.py @@ -4,10 +4,16 @@ from __future__ import annotations """Test cases for QtAsyncio""" -import unittest -import asyncio +import os import sys +import unittest +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +import asyncio import PySide6.QtAsyncio as QtAsyncio @@ -22,7 +28,7 @@ class QAsyncioTestCaseBug2799(unittest.TestCase): raise RuntimeError() def test_exception_group(self): - with self.assertRaises(ExceptionGroup): + with self.assertRaises(ExceptionGroup): # noqa: F821 QtAsyncio.run(self.main(), keep_running=False) diff --git a/sources/pyside6/tests/QtAsyncio/qasyncio_test.py b/sources/pyside6/tests/QtAsyncio/qasyncio_test.py index 80c5107a..cf3f7711 100644 --- a/sources/pyside6/tests/QtAsyncio/qasyncio_test.py +++ b/sources/pyside6/tests/QtAsyncio/qasyncio_test.py @@ -4,9 +4,16 @@ from __future__ import annotations '''Test cases for QtAsyncio''' +import os +import sys import unittest -import asyncio +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +import asyncio from PySide6.QtAsyncio import QAsyncioEventLoopPolicy diff --git a/sources/pyside6/tests/QtAsyncio/qasyncio_test_cancel_task.py b/sources/pyside6/tests/QtAsyncio/qasyncio_test_cancel_task.py index c2f56d01..ab28beb8 100644 --- a/sources/pyside6/tests/QtAsyncio/qasyncio_test_cancel_task.py +++ b/sources/pyside6/tests/QtAsyncio/qasyncio_test_cancel_task.py @@ -4,9 +4,16 @@ from __future__ import annotations '''Test cases for QtAsyncio''' -import asyncio +import os +import sys import unittest +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +import asyncio import PySide6.QtAsyncio as QtAsyncio diff --git a/sources/pyside6/tests/QtAsyncio/qasyncio_test_cancel_taskgroup.py b/sources/pyside6/tests/QtAsyncio/qasyncio_test_cancel_taskgroup.py index f3724f01..178701dc 100644 --- a/sources/pyside6/tests/QtAsyncio/qasyncio_test_cancel_taskgroup.py +++ b/sources/pyside6/tests/QtAsyncio/qasyncio_test_cancel_taskgroup.py @@ -4,10 +4,16 @@ from __future__ import annotations '''Test cases for QtAsyncio''' -import asyncio -import unittest +import os import sys +import unittest +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +import asyncio import PySide6.QtAsyncio as QtAsyncio diff --git a/sources/pyside6/tests/QtAsyncio/qasyncio_test_chain.py b/sources/pyside6/tests/QtAsyncio/qasyncio_test_chain.py index a8e5bb19..8b41a133 100644 --- a/sources/pyside6/tests/QtAsyncio/qasyncio_test_chain.py +++ b/sources/pyside6/tests/QtAsyncio/qasyncio_test_chain.py @@ -4,11 +4,18 @@ from __future__ import annotations '''Test cases for QtAsyncio''' +import os +import sys import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + import asyncio import random import time - from PySide6.QtAsyncio import QAsyncioEventLoopPolicy diff --git a/sources/pyside6/tests/QtAsyncio/qasyncio_test_executor.py b/sources/pyside6/tests/QtAsyncio/qasyncio_test_executor.py index 641d374d..f9812a77 100644 --- a/sources/pyside6/tests/QtAsyncio/qasyncio_test_executor.py +++ b/sources/pyside6/tests/QtAsyncio/qasyncio_test_executor.py @@ -4,11 +4,18 @@ from __future__ import annotations '''Test cases for QtAsyncio''' +import os +import sys import unittest -import asyncio from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +import asyncio from PySide6.QtCore import QThread from PySide6.QtAsyncio import QAsyncioEventLoopPolicy diff --git a/sources/pyside6/tests/QtAsyncio/qasyncio_test_queues.py b/sources/pyside6/tests/QtAsyncio/qasyncio_test_queues.py index 2df39ee0..f75480d9 100644 --- a/sources/pyside6/tests/QtAsyncio/qasyncio_test_queues.py +++ b/sources/pyside6/tests/QtAsyncio/qasyncio_test_queues.py @@ -4,11 +4,18 @@ from __future__ import annotations '''Test cases for QtAsyncio''' +import os +import sys import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + import asyncio import random import time - from PySide6.QtAsyncio import QAsyncioEventLoopPolicy diff --git a/sources/pyside6/tests/QtAsyncio/qasyncio_test_threadsafe.py b/sources/pyside6/tests/QtAsyncio/qasyncio_test_threadsafe.py index 48935434..a3bcd0c2 100644 --- a/sources/pyside6/tests/QtAsyncio/qasyncio_test_threadsafe.py +++ b/sources/pyside6/tests/QtAsyncio/qasyncio_test_threadsafe.py @@ -4,14 +4,22 @@ from __future__ import annotations '''Test cases for QtAsyncio''' +import os +import sys import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + import asyncio import threading import time - from PySide6.QtAsyncio import QAsyncioEventLoopPolicy +@unittest.skip("Temporarily disabled: hangs indefinitely on Python < 3.11") class QAsyncioTestCaseThreadsafe(unittest.TestCase): def setUp(self) -> None: diff --git a/sources/pyside6/tests/QtAsyncio/qasyncio_test_time.py b/sources/pyside6/tests/QtAsyncio/qasyncio_test_time.py index 66f0433d..e95e457b 100644 --- a/sources/pyside6/tests/QtAsyncio/qasyncio_test_time.py +++ b/sources/pyside6/tests/QtAsyncio/qasyncio_test_time.py @@ -4,10 +4,17 @@ from __future__ import annotations '''Test cases for QtAsyncio''' +import os +import sys import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + import asyncio import datetime - from PySide6.QtAsyncio import QAsyncioEventLoopPolicy diff --git a/sources/pyside6/tests/QtAsyncio/qasyncio_test_uncancel.py b/sources/pyside6/tests/QtAsyncio/qasyncio_test_uncancel.py index 03662284..fd75a1fc 100644 --- a/sources/pyside6/tests/QtAsyncio/qasyncio_test_uncancel.py +++ b/sources/pyside6/tests/QtAsyncio/qasyncio_test_uncancel.py @@ -4,12 +4,20 @@ from __future__ import annotations """Test cases for QtAsyncio""" +import os +import sys import unittest -import asyncio +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +import asyncio import PySide6.QtAsyncio as QtAsyncio +@unittest.skipIf(sys.version_info < (3, 11), "requires Task.uncancel() (Python 3.11+)") class QAsyncioTestCaseUncancel(unittest.TestCase): """ https://superfastpython.com/asyncio-cancel-task-cancellation """ diff --git a/sources/pyside6/tests/QtCore/CMakeLists.txt b/sources/pyside6/tests/QtCore/CMakeLists.txt index f4c9ded3..28db9616 100644 --- a/sources/pyside6/tests/QtCore/CMakeLists.txt +++ b/sources/pyside6/tests/QtCore/CMakeLists.txt @@ -73,11 +73,13 @@ PYSIDE_TEST(qdate_test.py) PYSIDE_TEST(qdir_test.py) PYSIDE_TEST(qeasingcurve_test.py) PYSIDE_TEST(qenum_test.py) +PYSIDE_TEST(qenum_designer_test.py) PYSIDE_TEST(qevent_test.py) PYSIDE_TEST(qfileinfo_test.py) PYSIDE_TEST(qfile_test.py) PYSIDE_TEST(qfileread_test.py) PYSIDE_TEST(qflags_test.py) +PYSIDE_TEST(qrangemodel_test.py) PYSIDE_TEST(qinstallmsghandler_test.py) PYSIDE_TEST(qiodevice_buffered_read_test.py) PYSIDE_TEST(qiopipe_test.py) diff --git a/sources/pyside6/tests/QtCore/deepcopy_test.py b/sources/pyside6/tests/QtCore/deepcopy_test.py index 8b211a97..64b001e6 100644 --- a/sources/pyside6/tests/QtCore/deepcopy_test.py +++ b/sources/pyside6/tests/QtCore/deepcopy_test.py @@ -12,8 +12,8 @@ sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) from init_paths import init_test_paths init_test_paths(False) -from PySide6.QtCore import QByteArray, QDate, QDateTime, QTime, QLine, QLineF -from PySide6.QtCore import Qt, QSize, QSizeF, QRect, QRectF, QDir, QPoint, QPointF +from PySide6.QtCore import QByteArray, QDate, QDateTime, QTime, QLine, QLineF, QTimeZone +from PySide6.QtCore import QSize, QSizeF, QRect, QRectF, QDir, QPoint, QPointF try: from PySide6.QtCore import QUuid HAVE_Q = True @@ -45,7 +45,7 @@ class QTimeDeepCopy(DeepCopyHelper, unittest.TestCase): class QDateTimeDeepCopy(DeepCopyHelper, unittest.TestCase): def setUp(self): - self.original = QDateTime(2010, 5, 18, 10, 24, 45, 223, Qt.TimeSpec.LocalTime) + self.original = QDateTime(2010, 5, 18, 10, 24, 45, 223, QTimeZone(QTimeZone.LocalTime)) class QSizeDeepCopy(DeepCopyHelper, unittest.TestCase): diff --git a/sources/pyside6/tests/QtCore/qenum_designer_test.py b/sources/pyside6/tests/QtCore/qenum_designer_test.py new file mode 100644 index 00000000..2a8f3526 --- /dev/null +++ b/sources/pyside6/tests/QtCore/qenum_designer_test.py @@ -0,0 +1,136 @@ +#!/usr/bin/python +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +'''Test cases for QEnum and QFlags within Qt Widgets Designer''' + +import os +import sys +import unittest + +from enum import Enum, Flag + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Property, QEnum, QFlag, Qt + + +class CustomWidgetBase(QObject): + @QEnum + class TestEnum(Enum): + EnumValue0 = 0 + EnumValue1 = 1 + EnumValue2 = 2 + EnumValue3 = 3 + + @QFlag + class TestFlag(Flag): + FlagValue0 = 1 + FlagValue1 = 2 + FlagValue2 = 4 + FlagValue3 = 8 + + @QFlag + class BigTestFlag(Flag): + BigFlagValue0 = 0x100000000 # >32bit + BigFlagValue1 = 0x200000000 + BigFlagValue2 = 0x400000000 + BigFlagValue3 = 0x800000000 + + +class CustomWidget(CustomWidgetBase): + def __init__(self, parent=None): + super().__init__(parent) + self._testEnum = CustomWidget.TestEnum.EnumValue1 + self._testFlag = (CustomWidget.TestFlag.FlagValue0 + | CustomWidget.TestFlag.FlagValue1) + self._bigTestFlag = CustomWidget.BigTestFlag.BigFlagValue1 + self._orientation = Qt.Orientation.Horizontal + + def orientation(self): + return self._orientation + + def setOrientation(self, new_val): + self._orientation = new_val + + def testEnum(self): + return self._testEnum + + def setTestEnum(self, new_val): + self._testEnum = new_val + + def getTestFlag(self): + return self._testFlag + + def setTestFlag(self, new_val): + self._testFlag = new_val + + def getBigTestFlag(self): + return self._bigTestFlag + + def setBigTestFlag(self, new_val): + self._bigTestFlag = new_val + + # Qt C++ enum + orientation = Property(Qt.Orientation, orientation, setOrientation) + # Decorated Python enums + testEnum = Property(CustomWidgetBase.TestEnum, testEnum, setTestEnum) + testFlag = Property(CustomWidgetBase.TestFlag, getTestFlag, setTestFlag) + bigTestFlag = Property(CustomWidgetBase.BigTestFlag, + getBigTestFlag, setBigTestFlag) + + +class TestDesignerEnum(unittest.TestCase): + """PYSIDE-2840: Test whether a custom widget with decorated enum/flag properties + allows for modifying the values from C++.""" + + def setUp(self): + super().setUp() + self._cw = CustomWidget() + + def tearDown(self): + super().tearDown() + self._cw = None + + def testEnum(self): + # Emulate Qt Widgets Designer setting a property + cw = self._cw + cw.setProperty("testEnum", 3) + self.assertEqual(cw.testEnum, CustomWidget.TestEnum.EnumValue3) + # Emulate uic generated code + cw.setProperty("testEnum", CustomWidgetBase.TestEnum.EnumValue2) + self.assertEqual(cw.testEnum, CustomWidget.TestEnum.EnumValue2) + + # Emulate Qt Widgets Designer setting a property + cw.setProperty("testFlag", 12) + self.assertEqual(cw.testFlag, (CustomWidget.TestFlag.FlagValue2 + | CustomWidget.TestFlag.FlagValue3)) + # Emulate uic generated code + cw.setProperty("testFlag", CustomWidgetBase.TestFlag.FlagValue1) + self.assertEqual(cw.testFlag, CustomWidget.TestFlag.FlagValue1) + + # Emulate Qt Widgets Designer setting a property (note though + # it does not support it). + self.assertEqual(cw.bigTestFlag, CustomWidget.BigTestFlag.BigFlagValue1) + ok = cw.setProperty("bigTestFlag", CustomWidgetBase.BigTestFlag.BigFlagValue2) + self.assertTrue(ok) + self.assertEqual(cw.bigTestFlag, CustomWidget.BigTestFlag.BigFlagValue2) + + def testMetaProperty(self): + mo = self._cw.metaObject() + index = mo.indexOfProperty("orientation") + self.assertTrue(index != -1) + self.assertTrue(mo.property(index).isEnumType()) + index = mo.indexOfProperty("testEnum") + self.assertTrue(index != -1) + self.assertTrue(mo.property(index).isEnumType()) + index = mo.indexOfProperty("testFlag") + self.assertTrue(index != -1) + self.assertTrue(mo.property(index).isEnumType()) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/QtCore/qenum_test.py b/sources/pyside6/tests/QtCore/qenum_test.py index 58115295..edf22dfa 100644 --- a/sources/pyside6/tests/QtCore/qenum_test.py +++ b/sources/pyside6/tests/QtCore/qenum_test.py @@ -135,6 +135,9 @@ class SomeClass(QObject): A = 1 B = 2 C = 3 + D = 0x100000000 # >32bit + E = 0x200000000 + F = 0x400000000 class InnerClass(QObject): @@ -186,9 +189,39 @@ class TestQEnumMacro(unittest.TestCase): def testIsRegistered(self): mo = SomeClass.staticMetaObject self.assertEqual(mo.enumeratorCount(), 2) - self.assertEqual(mo.enumerator(0).name(), "OtherEnum") - self.assertEqual(mo.enumerator(0).scope(), "SomeClass") - self.assertEqual(mo.enumerator(1).name(), "SomeEnum") + + # 64 bit / IntEnum + other_metaenum = mo.enumerator(0) + self.assertEqual(other_metaenum.scope(), "SomeClass") + self.assertEqual(other_metaenum.name(), "OtherEnum") + self.assertTrue(other_metaenum.is64Bit()) + key_count = other_metaenum.keyCount() + self.assertEqual(key_count, 6) + self.assertEqual(other_metaenum.value(key_count - 1), SomeClass.OtherEnum.F) + # Test lookup + v, ok = other_metaenum.keyToValue("F") + self.assertTrue(ok) + self.assertEqual(v, SomeClass.OtherEnum.F) + v, ok = other_metaenum.keysToValue("E") + self.assertTrue(ok) + self.assertEqual(v, SomeClass.OtherEnum.E) + + # 32 bit / Enum + some_metaenum = mo.enumerator(1) + self.assertEqual(some_metaenum.scope(), "SomeClass") + self.assertEqual(some_metaenum.name(), "SomeEnum") + self.assertFalse(some_metaenum.is64Bit()) + key_count = some_metaenum.keyCount() + self.assertEqual(key_count, 3) + self.assertEqual(some_metaenum.value(key_count - 1), SomeClass.SomeEnum.C.value) + # Test lookup + v, ok = some_metaenum.keyToValue("C") + self.assertTrue(ok) + self.assertEqual(v, SomeClass.SomeEnum.C.value) + v, ok = some_metaenum.keysToValue("C") + self.assertTrue(ok) + self.assertEqual(v, SomeClass.SomeEnum.C.value) + moi = SomeClass.InnerClass.staticMetaObject self.assertEqual(moi.enumerator(0).name(), "InnerEnum") # Question: Should that scope not better be "SomeClass.InnerClass"? diff --git a/sources/pyside6/tests/QtCore/qobject_event_filter_test.py b/sources/pyside6/tests/QtCore/qobject_event_filter_test.py index a011f724..2ab5032d 100644 --- a/sources/pyside6/tests/QtCore/qobject_event_filter_test.py +++ b/sources/pyside6/tests/QtCore/qobject_event_filter_test.py @@ -114,14 +114,15 @@ class TestQObjectEventFilterPython(UsesQApplication): def testInstallEventFilterRefCountAfterDelete(self): '''Bug 910 - installEventFilter() increments reference count on target object http://bugs.pyside.org/show_bug.cgi?id=910''' + expected_ref_count = 1 if sys.version_info >= (3, 14) else 2 obj = QObject() filt = QObject() - self.assertEqual(sys.getrefcount(obj), 2) - self.assertEqual(sys.getrefcount(filt), 2) + self.assertEqual(sys.getrefcount(obj), expected_ref_count) + self.assertEqual(sys.getrefcount(filt), expected_ref_count) obj.installEventFilter(filt) - self.assertEqual(sys.getrefcount(obj), 2) - self.assertEqual(sys.getrefcount(filt), 2) + self.assertEqual(sys.getrefcount(obj), expected_ref_count) + self.assertEqual(sys.getrefcount(filt), expected_ref_count) wref = weakref.ref(obj) del obj @@ -133,14 +134,15 @@ class TestQObjectEventFilterPython(UsesQApplication): obj = QObject() filt = QObject() - self.assertEqual(sys.getrefcount(obj), 2) - self.assertEqual(sys.getrefcount(filt), 2) + expected_ref_count = 1 if sys.version_info >= (3, 14) else 2 + self.assertEqual(sys.getrefcount(obj), expected_ref_count) + self.assertEqual(sys.getrefcount(filt), expected_ref_count) obj.installEventFilter(filt) - self.assertEqual(sys.getrefcount(obj), 2) - self.assertEqual(sys.getrefcount(filt), 2) + self.assertEqual(sys.getrefcount(obj), expected_ref_count) + self.assertEqual(sys.getrefcount(filt), expected_ref_count) obj.removeEventFilter(filt) - self.assertEqual(sys.getrefcount(obj), 2) - self.assertEqual(sys.getrefcount(filt), 2) + self.assertEqual(sys.getrefcount(obj), expected_ref_count) + self.assertEqual(sys.getrefcount(filt), expected_ref_count) wref = weakref.ref(obj) del obj diff --git a/sources/pyside6/tests/QtCore/qobject_parent_test.py b/sources/pyside6/tests/QtCore/qobject_parent_test.py index ffca0d17..932f7864 100644 --- a/sources/pyside6/tests/QtCore/qobject_parent_test.py +++ b/sources/pyside6/tests/QtCore/qobject_parent_test.py @@ -61,7 +61,8 @@ class ParentRefCountCase(unittest.TestCase): def testConstructor(self): # QObject(QObject) refcount changes child = QObject(self.parent) - self.assertEqual(sys.getrefcount(child), 3) + expected_ref_count = 2 if sys.version_info >= (3, 14) else 3 + self.assertEqual(sys.getrefcount(child), expected_ref_count) class ParentCase(unittest.TestCase): diff --git a/sources/pyside6/tests/QtCore/qobject_property_test.py b/sources/pyside6/tests/QtCore/qobject_property_test.py index 80387ec7..e0a8044f 100644 --- a/sources/pyside6/tests/QtCore/qobject_property_test.py +++ b/sources/pyside6/tests/QtCore/qobject_property_test.py @@ -9,6 +9,7 @@ import sys import unittest from pathlib import Path +from typing import NamedTuple sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) from init_paths import init_test_paths init_test_paths(False) @@ -16,6 +17,9 @@ init_test_paths(False) from PySide6.QtCore import QObject, Property, Signal +Point = NamedTuple("Point", [("x", float), ("y", float)]) + + class MyObjectWithNotifyProperty(QObject): def __init__(self, parent=None): QObject.__init__(self, parent) @@ -52,6 +56,23 @@ class MyObjectWithOtherClassProperty(QObject): otherclass = Property(OtherClass, fget=_get_otherclass, fset=_set_otherclass) +class TestVariantPropertyObject(QObject): + """Helper for testing QVariant conversion in properties and signals + (PYSIDE-3206, PYSIDE-3244). It uses a property of list type that + can passed a QVariant list with various element types.""" + def __init__(self, parent=None): + super().__init__(parent) + self._property = None + + def set_property(self, v): + self._property = v + + def get_property(self): + return self._property + + testProperty = Property(list, fget=get_property, fset=set_property) + + class PropertyWithNotify(unittest.TestCase): def called(self): self.called_ = True @@ -84,5 +105,33 @@ class QObjectWithOtherClassPropertyTest(unittest.TestCase): self.assertTrue(type(pv) is OtherClass) +class VariantPropertyTest(unittest.TestCase): + """Test QVariant conversion in properties and signals (PYSIDE-3256, + PYSIDE-3244, PYSIDE-3206 [open]). It uses a property of list type + that is passed a QVariantList with various element types when + using QObject.setProperty().""" + + def testIt(self): + to = TestVariantPropertyObject() + idx = to.metaObject().indexOfProperty("testProperty") + self.assertTrue(idx != -1) + + # List + to.setProperty("testProperty", [[1, 2]]) + self.assertEqual(type(to.get_property()[0]), list) + + # Dict + to.setProperty("testProperty", [{"key": 42}]) + self.assertEqual(type(to.get_property()[0]), dict) + + # Tuple (PYSIDE-3256) + to.setProperty("testProperty", [(1, 2)]) + self.assertEqual(type(to.get_property()[0]), tuple) + + # Named Tuple (PYSIDE-3244) + to.setProperty("testProperty", [Point(1, 2)]) + self.assertEqual(type(to.get_property()[0]), Point) + + if __name__ == '__main__': unittest.main() diff --git a/sources/pyside6/tests/QtCore/qrangemodel_test.py b/sources/pyside6/tests/QtCore/qrangemodel_test.py new file mode 100644 index 00000000..b9319bd8 --- /dev/null +++ b/sources/pyside6/tests/QtCore/qrangemodel_test.py @@ -0,0 +1,56 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +from __future__ import annotations + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QRangeModel + + +try: + import numpy as np + HAVE_NUMPY = True +except ModuleNotFoundError: + HAVE_NUMPY = False + + +class QRangeModelTest(unittest.TestCase): + + def test_pylist(self): + test_list = [1, 2, 3] + model = QRangeModel(test_list) + self.assertEqual(model.rowCount(), 3) + self.assertEqual(model.data(model.createIndex(2, 0)), 3) + + def test_pytable(self): + test_table = [[1, 2], [3, 4]] + model = QRangeModel(test_table) + self.assertEqual(model.rowCount(), 2) + self.assertEqual(model.columnCount(), 2) + self.assertEqual(model.data(model.createIndex(1, 1)), 4) + + @unittest.skipUnless(HAVE_NUMPY, "requires numpy") + def test_numpy_list(self): + test_array = np.array([1, 2, 3]) + model = QRangeModel(test_array) + self.assertEqual(model.rowCount(), 3) + self.assertEqual(model.data(model.createIndex(2, 0)), 3) + + @unittest.skipUnless(HAVE_NUMPY, "requires numpy") + def test_numpy_table(self): + test_table = np.array([[1, 2], [3, 4]]) + model = QRangeModel(test_table) + self.assertEqual(model.rowCount(), 2) + self.assertEqual(model.columnCount(), 2) + self.assertEqual(model.data(model.createIndex(1, 1)), 4) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/QtCore/repr_test.py b/sources/pyside6/tests/QtCore/repr_test.py index 084b69f8..52a426d3 100644 --- a/sources/pyside6/tests/QtCore/repr_test.py +++ b/sources/pyside6/tests/QtCore/repr_test.py @@ -13,8 +13,8 @@ init_test_paths(False) # for 'self.original' import PySide6 # noqa -from PySide6.QtCore import QByteArray, QDate, QDateTime, QTime, QLine, QLineF -from PySide6.QtCore import Qt, QSize, QSizeF, QRect, QRectF, QPoint, QPointF +from PySide6.QtCore import QByteArray, QDate, QDateTime, QTime, QLine, QLineF, QTimeZone +from PySide6.QtCore import QSize, QSizeF, QRect, QRectF, QPoint, QPointF try: from PySide6.QtCore import QUuid HAVE_Q = True @@ -46,7 +46,7 @@ class QTimeReprCopy(ReprCopyHelper, unittest.TestCase): class QDateTimeReprCopy(ReprCopyHelper, unittest.TestCase): def setUp(self): - self.original = QDateTime(2010, 5, 18, 10, 24, 45, 223, Qt.TimeSpec.LocalTime) + self.original = QDateTime(2010, 5, 18, 10, 24, 45, 223, QTimeZone(QTimeZone.LocalTime)) class QSizeReprCopy(ReprCopyHelper, unittest.TestCase): diff --git a/sources/pyside6/tests/QtCore/tr_noop_test.py b/sources/pyside6/tests/QtCore/tr_noop_test.py index ffa47cd5..36bbc6b4 100644 --- a/sources/pyside6/tests/QtCore/tr_noop_test.py +++ b/sources/pyside6/tests/QtCore/tr_noop_test.py @@ -16,6 +16,9 @@ from PySide6.QtCore import QT_TR_NOOP, QT_TR_NOOP_UTF8 from PySide6.QtCore import QT_TRANSLATE_NOOP, QT_TRANSLATE_NOOP3, QT_TRANSLATE_NOOP_UTF8 +REF_COUNT_DELTA = 0 if sys.version_info >= (3, 14) else 1 # 3.14 borrows references + + class QtTrNoopTest(unittest.TestCase): def setUp(self): @@ -31,35 +34,35 @@ class QtTrNoopTest(unittest.TestCase): refcnt = sys.getrefcount(self.txt) result = QT_TR_NOOP(self.txt) self.assertEqual(result, self.txt) - self.assertEqual(sys.getrefcount(result), refcnt + 1) + self.assertEqual(sys.getrefcount(result), refcnt + REF_COUNT_DELTA) @unittest.skipUnless(hasattr(sys, "getrefcount"), f"{sys.implementation.name} has no refcount") def testQtTrNoopUtf8(self): refcnt = sys.getrefcount(self.txt) result = QT_TR_NOOP_UTF8(self.txt) self.assertEqual(result, self.txt) - self.assertEqual(sys.getrefcount(result), refcnt + 1) + self.assertEqual(sys.getrefcount(result), refcnt + REF_COUNT_DELTA) @unittest.skipUnless(hasattr(sys, "getrefcount"), f"{sys.implementation.name} has no refcount") def testQtTranslateNoop(self): refcnt = sys.getrefcount(self.txt) result = QT_TRANSLATE_NOOP(None, self.txt) self.assertEqual(result, self.txt) - self.assertEqual(sys.getrefcount(result), refcnt + 1) + self.assertEqual(sys.getrefcount(result), refcnt + REF_COUNT_DELTA) @unittest.skipUnless(hasattr(sys, "getrefcount"), f"{sys.implementation.name} has no refcount") def testQtTranslateNoopUtf8(self): refcnt = sys.getrefcount(self.txt) result = QT_TRANSLATE_NOOP_UTF8(self.txt) self.assertEqual(result, self.txt) - self.assertEqual(sys.getrefcount(result), refcnt + 1) + self.assertEqual(sys.getrefcount(result), refcnt + REF_COUNT_DELTA) @unittest.skipUnless(hasattr(sys, "getrefcount"), f"{sys.implementation.name} has no refcount") def testQtTranslateNoop3(self): refcnt = sys.getrefcount(self.txt) result = QT_TRANSLATE_NOOP3(None, self.txt, None) self.assertEqual(result, self.txt) - self.assertEqual(sys.getrefcount(result), refcnt + 1) + self.assertEqual(sys.getrefcount(result), refcnt + REF_COUNT_DELTA) if __name__ == '__main__': diff --git a/sources/pyside6/tests/QtDBus/CMakeLists.txt b/sources/pyside6/tests/QtDBus/CMakeLists.txt index bf2d2105..e618fd7b 100644 --- a/sources/pyside6/tests/QtDBus/CMakeLists.txt +++ b/sources/pyside6/tests/QtDBus/CMakeLists.txt @@ -1,6 +1,6 @@ # Copyright (C) 2023 The Qt Company Ltd. # SPDX-License-Identifier: BSD-3-Clause -if(ENABLE_UNIX) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") PYSIDE_TEST(test_dbus.py) endif() diff --git a/sources/pyside6/tests/QtGui/CMakeLists.txt b/sources/pyside6/tests/QtGui/CMakeLists.txt index fb0df142..c8ac4c45 100644 --- a/sources/pyside6/tests/QtGui/CMakeLists.txt +++ b/sources/pyside6/tests/QtGui/CMakeLists.txt @@ -19,6 +19,7 @@ PYSIDE_TEST(bug_PYSIDE-344.py) PYSIDE_TEST(deepcopy_test.py) PYSIDE_TEST(event_filter_test.py) PYSIDE_TEST(float_to_int_implicit_conversion_test.py) +PYSIDE_TEST(nativeinterface_test.py) PYSIDE_TEST(qbrush_test.py) PYSIDE_TEST(qcolor_test.py) PYSIDE_TEST(qcolor_reduce_test.py) diff --git a/sources/pyside6/tests/QtGui/nativeinterface_test.py b/sources/pyside6/tests/QtGui/nativeinterface_test.py new file mode 100644 index 00000000..6822544f --- /dev/null +++ b/sources/pyside6/tests/QtGui/nativeinterface_test.py @@ -0,0 +1,32 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths # noqa: E402 +init_test_paths(False) + +from PySide6.QtGui import QGuiApplication, QNativeInterface # noqa +from helper.usesqapplication import UsesQApplication # noqa: E402 + + +class TestNativeInterface(UsesQApplication): + + @unittest.skipUnless(sys.platform == "linux", "Linux only") + def testLinuxNativeApplication(self): + app = qApp # noqa: F821 + native_app = app.nativeInterface() + if native_app: + if issubclass(type(native_app), QNativeInterface.QX11Application): + self.assertTrue(native_app.display() != 0) + elif issubclass(type(native_app), QNativeInterface.QWaylandApplication): + self.assertTrue(native_app.display() != 0) + self.assertTrue(native_app.seat() is not None) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/QtGui/qmatrix_test.py b/sources/pyside6/tests/QtGui/qmatrix_test.py new file mode 100644 index 00000000..64a97d6b --- /dev/null +++ b/sources/pyside6/tests/QtGui/qmatrix_test.py @@ -0,0 +1,68 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +from __future__ import annotations + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtGui import ( + QMatrix2x2, + QMatrix2x3, + QMatrix2x4, + QMatrix3x2, + QMatrix3x3, + QMatrix3x4, + QMatrix4x2, + QMatrix4x3, + QMatrix4x4, +) + + +# Helper function to create sequential data for a matrix +def sequential_values(rows: int, cols: int) -> list[float]: + return [float(i + 1) for i in range(rows * cols)] + + +class TestQMatrixIndexing(unittest.TestCase): + + def setUp(self): + # Matrix types + self.matrices = [ + (QMatrix2x2(sequential_values(2, 2)), 2, 2), + (QMatrix2x3(sequential_values(2, 3)), 3, 2), + (QMatrix2x4(sequential_values(2, 4)), 4, 2), + (QMatrix3x2(sequential_values(3, 2)), 2, 3), + (QMatrix3x3(sequential_values(3, 3)), 3, 3), + (QMatrix3x4(sequential_values(3, 4)), 4, 3), + (QMatrix4x2(sequential_values(4, 2)), 2, 4), + (QMatrix4x3(sequential_values(4, 3)), 3, 4), + (QMatrix4x4(sequential_values(4, 4)), 4, 4), + ] + + def test_getitem(self): + """Test [row, col] indexing for all matrix types.""" + for m, rows, cols in self.matrices: + v = 1.0 + for x in range(rows): + for y in range(cols): + self.assertEqual(m[x, y], v) + v += 1.0 + + def test_callable_operator(self): + """Test operator()(row, col) for all QMatrix types.""" + for m, rows, cols in self.matrices: + v = 1.0 + for x in range(rows): + for y in range(cols): + self.assertEqual(m(x, y), v) + v += 1.0 + + +if __name__ == "__main__": + unittest.main() diff --git a/sources/pyside6/tests/QtGui/qpolygonf_test.py b/sources/pyside6/tests/QtGui/qpolygonf_test.py index ea5a73db..85325f11 100644 --- a/sources/pyside6/tests/QtGui/qpolygonf_test.py +++ b/sources/pyside6/tests/QtGui/qpolygonf_test.py @@ -38,6 +38,18 @@ class QPolygonFNotIterableTest(unittest.TestCase): p << QPoint(10, 20) << QPoint(20, 30) << [QPoint(20, 30), QPoint(40, 50)] self.assertEqual(len(p), 4) + def testPolygonComparassion(self): + points = [QPointF(10, 20), QPointF(20, 30), QPointF(20, 30), QPointF(40, 50)] + + p = QPolygonF(points) + other = QPolygonF(points) + self.assertEqual(p, other) + + points = [QPointF(10, 10), QPointF(20, 20), QPointF(30, 30), QPointF(40, 40)] + polygon = QPolygonF(points) + self.assertNotEqual(p, polygon) + self.assertNotEqual(other, polygon) + if __name__ == '__main__': unittest.main() diff --git a/sources/pyside6/tests/QtGui/qstandarditemmodel_test.py b/sources/pyside6/tests/QtGui/qstandarditemmodel_test.py index 683b5825..21de4a75 100644 --- a/sources/pyside6/tests/QtGui/qstandarditemmodel_test.py +++ b/sources/pyside6/tests/QtGui/qstandarditemmodel_test.py @@ -74,11 +74,12 @@ class QStandardItemModelRef(UsesQApplication): my_i = model.item(0, 0) # ref(my_i) + parent_ref + items list ref - self.assertEqual(sys.getrefcount(my_i), 4) + base_ref_count = 2 if sys.version_info >= (3, 14) else 3 + self.assertEqual(sys.getrefcount(my_i), base_ref_count + 1) model.clear() # ref(my_i) - self.assertEqual(sys.getrefcount(my_i), 3) + self.assertEqual(sys.getrefcount(my_i), base_ref_count) if __name__ == '__main__': diff --git a/sources/pyside6/tests/QtStateMachine/qabstracttransition_test.py b/sources/pyside6/tests/QtStateMachine/qabstracttransition_test.py index b7f346b2..ceec27d2 100644 --- a/sources/pyside6/tests/QtStateMachine/qabstracttransition_test.py +++ b/sources/pyside6/tests/QtStateMachine/qabstracttransition_test.py @@ -17,6 +17,9 @@ from PySide6.QtCore import (QCoreApplication, QObject, QParallelAnimationGroup, from PySide6.QtStateMachine import QEventTransition, QFinalState, QState, QStateMachine +REF_COUNT_DELTA = 2 if sys.version_info >= (3, 14) else 1 + + def addStates(transition): sx = QState() sy = QState() @@ -73,7 +76,8 @@ class QAbstractTransitionTest(unittest.TestCase): transition.setTargetState(state1) self.assertEqual(transition.targetState(), state1) - self.assertEqual(sys.getrefcount(transition.targetState()), refcount1 + 1) + self.assertEqual(sys.getrefcount(transition.targetState()), + refcount1 + REF_COUNT_DELTA) state2 = QState() refcount2 = sys.getrefcount(state2) @@ -81,7 +85,8 @@ class QAbstractTransitionTest(unittest.TestCase): transition.setTargetState(state2) self.assertEqual(transition.targetState(), state2) - self.assertEqual(sys.getrefcount(transition.targetState()), refcount2 + 1) + self.assertEqual(sys.getrefcount(transition.targetState()), + refcount2 + REF_COUNT_DELTA) self.assertEqual(sys.getrefcount(state1), refcount1) del transition @@ -101,8 +106,10 @@ class QAbstractTransitionTest(unittest.TestCase): self.assertEqual(transition.targetStates(), states) self.assertEqual(transition.targetState(), state1) - self.assertEqual(sys.getrefcount(transition.targetStates()[0]), refcount1 + 1) - self.assertEqual(sys.getrefcount(transition.targetStates()[1]), refcount2 + 1) + self.assertEqual(sys.getrefcount(transition.targetStates()[0]), + refcount1 + REF_COUNT_DELTA) + self.assertEqual(sys.getrefcount(transition.targetStates()[1]), + refcount2 + REF_COUNT_DELTA) del states del transition @@ -119,7 +126,8 @@ class QAbstractTransitionTest(unittest.TestCase): transition.setTargetState(state0) self.assertEqual(transition.targetState(), state0) - self.assertEqual(sys.getrefcount(transition.targetState()), refcount0 + 1) + self.assertEqual(sys.getrefcount(transition.targetState()), + refcount0 + REF_COUNT_DELTA) state1 = QState() state2 = QState() @@ -132,8 +140,10 @@ class QAbstractTransitionTest(unittest.TestCase): self.assertEqual(sys.getrefcount(state0), refcount0) self.assertEqual(transition.targetStates(), states) self.assertEqual(transition.targetState(), state1) - self.assertEqual(sys.getrefcount(transition.targetStates()[0]), refcount1 + 1) - self.assertEqual(sys.getrefcount(transition.targetStates()[1]), refcount2 + 1) + self.assertEqual(sys.getrefcount(transition.targetStates()[0]), + refcount1 + REF_COUNT_DELTA) + self.assertEqual(sys.getrefcount(transition.targetStates()[1]), + refcount2 + REF_COUNT_DELTA) del states del transition @@ -154,8 +164,10 @@ class QAbstractTransitionTest(unittest.TestCase): self.assertEqual(transition.targetStates(), states) self.assertEqual(transition.targetState(), state1) - self.assertEqual(sys.getrefcount(transition.targetStates()[0]), refcount1 + 1) - self.assertEqual(sys.getrefcount(transition.targetStates()[1]), refcount2 + 1) + self.assertEqual(sys.getrefcount(transition.targetStates()[0]), + refcount1 + REF_COUNT_DELTA) + self.assertEqual(sys.getrefcount(transition.targetStates()[1]), + refcount2 + REF_COUNT_DELTA) state3 = QState() refcount3 = sys.getrefcount(state3) @@ -163,7 +175,8 @@ class QAbstractTransitionTest(unittest.TestCase): transition.setTargetState(state3) self.assertEqual(transition.targetState(), state3) - self.assertEqual(sys.getrefcount(transition.targetState()), refcount3 + 1) + self.assertEqual(sys.getrefcount(transition.targetState()), + refcount3 + REF_COUNT_DELTA) del states diff --git a/sources/pyside6/tests/QtSvg/qsvggenerator_test.py b/sources/pyside6/tests/QtSvg/qsvggenerator_test.py index 4d9bf472..544732cc 100644 --- a/sources/pyside6/tests/QtSvg/qsvggenerator_test.py +++ b/sources/pyside6/tests/QtSvg/qsvggenerator_test.py @@ -16,6 +16,9 @@ from PySide6.QtCore import QBuffer from PySide6.QtSvg import QSvgGenerator +REF_COUNT_DELTA = 2 if sys.version_info >= (3, 14) else 1 + + class QSvgGeneratorTest(unittest.TestCase): @unittest.skipUnless(hasattr(sys, "getrefcount"), f"{sys.implementation.name} has no refcount") @@ -27,7 +30,8 @@ class QSvgGeneratorTest(unittest.TestCase): generator.setOutputDevice(iodevice1) self.assertEqual(generator.outputDevice(), iodevice1) - self.assertEqual(sys.getrefcount(generator.outputDevice()), refcount1 + 1) + self.assertEqual(sys.getrefcount(generator.outputDevice()), + refcount1 + REF_COUNT_DELTA) iodevice2 = QBuffer() refcount2 = sys.getrefcount(iodevice2) @@ -35,7 +39,8 @@ class QSvgGeneratorTest(unittest.TestCase): generator.setOutputDevice(iodevice2) self.assertEqual(generator.outputDevice(), iodevice2) - self.assertEqual(sys.getrefcount(generator.outputDevice()), refcount2 + 1) + self.assertEqual(sys.getrefcount(generator.outputDevice()), + refcount2 + REF_COUNT_DELTA) self.assertEqual(sys.getrefcount(iodevice1), refcount1) del generator diff --git a/sources/pyside6/tests/QtUiTools/bug_909.py b/sources/pyside6/tests/QtUiTools/bug_909.py index e1a1df8f..1be2936a 100644 --- a/sources/pyside6/tests/QtUiTools/bug_909.py +++ b/sources/pyside6/tests/QtUiTools/bug_909.py @@ -26,7 +26,8 @@ class TestDestruction(UsesQApplication): fileName = QFile(file) loader = QUiLoader() main_win = loader.load(fileName) - self.assertEqual(sys.getrefcount(main_win), 2) + expected_ref_count = 1 if sys.version_info >= (3, 14) else 2 + self.assertEqual(sys.getrefcount(main_win), expected_ref_count) fileName.close() tw = QTabWidget(main_win) diff --git a/sources/pyside6/tests/QtWidgets/bug_750.py b/sources/pyside6/tests/QtWidgets/bug_750.py index 61356c17..7ede387e 100644 --- a/sources/pyside6/tests/QtWidgets/bug_750.py +++ b/sources/pyside6/tests/QtWidgets/bug_750.py @@ -13,7 +13,7 @@ init_test_paths(False) from helper.usesqapplication import UsesQApplication -from PySide6.QtCore import QCoreApplication, QTimer +from PySide6.QtCore import QTimer from PySide6.QtGui import QPainter from PySide6.QtWidgets import QWidget @@ -33,8 +33,6 @@ class TestQPainter(UsesQApplication): def testFontInfo(self): w = MyWidget() w.show() - while not w.windowHandle().isExposed(): - QCoreApplication.processEvents() self.app.exec() self.assertTrue(w._info) diff --git a/sources/pyside6/tests/QtWidgets/bug_811.py b/sources/pyside6/tests/QtWidgets/bug_811.py index 3e6ceb7b..60d168b6 100644 --- a/sources/pyside6/tests/QtWidgets/bug_811.py +++ b/sources/pyside6/tests/QtWidgets/bug_811.py @@ -33,14 +33,15 @@ class TestUserDataRefCount(UsesQApplication): cursor = QTextCursor(doc) cursor.insertText("PySide Rocks") ud = TestUserData({"Life": 42}) - self.assertEqual(sys.getrefcount(ud), 2) + base_ref_count = sys.getrefcount(ud) cursor.block().setUserData(ud) - self.assertEqual(sys.getrefcount(ud), 3) + self.assertEqual(sys.getrefcount(ud), base_ref_count + 1) ud2 = cursor.block().userData() - self.assertEqual(sys.getrefcount(ud), 4) + self.assertEqual(sys.getrefcount(ud), base_ref_count + 2) self.udata = weakref.ref(ud, None) del ud, ud2 - self.assertEqual(sys.getrefcount(self.udata()), 2) + delta = 1 if sys.version_info >= (3, 14) else 0 + self.assertEqual(sys.getrefcount(self.udata()), base_ref_count + delta) if __name__ == '__main__': diff --git a/sources/pyside6/tests/QtWidgets/keep_reference_test.py b/sources/pyside6/tests/QtWidgets/keep_reference_test.py index f93770aa..e9b96227 100644 --- a/sources/pyside6/tests/QtWidgets/keep_reference_test.py +++ b/sources/pyside6/tests/QtWidgets/keep_reference_test.py @@ -54,6 +54,8 @@ class KeepReferenceTest(UsesQApplication): refcount1 = sys.getrefcount(model1) view1 = QTableView() view1.setModel(model1) + if sys.version_info >= (3, 14): + refcount1 += 1 self.assertEqual(sys.getrefcount(view1.model()), refcount1 + 1) view2 = QTableView() @@ -69,9 +71,10 @@ class KeepReferenceTest(UsesQApplication): '''Tests reference count of model object referred by deceased view object.''' model = TestModel() refcount1 = sys.getrefcount(model) + delta = 2 if sys.version_info >= (3, 14) else 1 view = QTableView() view.setModel(model) - self.assertEqual(sys.getrefcount(view.model()), refcount1 + 1) + self.assertEqual(sys.getrefcount(view.model()), refcount1 + delta) del view self.assertEqual(sys.getrefcount(model), refcount1) diff --git a/sources/pyside6/tests/QtWidgets/qaccessible_test.py b/sources/pyside6/tests/QtWidgets/qaccessible_test.py index 5fdb64d0..fbf0beeb 100644 --- a/sources/pyside6/tests/QtWidgets/qaccessible_test.py +++ b/sources/pyside6/tests/QtWidgets/qaccessible_test.py @@ -127,6 +127,7 @@ class QAccessibleTest(UsesQApplication): QAccessible.installFactory(accessible_factory) window = Window() # noqa: F841 + @unittest.skipUnless(QAccessible.isActive(), "Accessibility is not active") def testLineEdits(self): window = Window() window.show() diff --git a/sources/pyside6/tests/QtWidgets/qpicture_test.py b/sources/pyside6/tests/QtWidgets/qpicture_test.py index e9b0440c..e1231465 100644 --- a/sources/pyside6/tests/QtWidgets/qpicture_test.py +++ b/sources/pyside6/tests/QtWidgets/qpicture_test.py @@ -12,7 +12,7 @@ from init_paths import init_test_paths init_test_paths(False) from helper.usesqapplication import UsesQApplication -from PySide6.QtCore import QCoreApplication, QTimer +from PySide6.QtCore import QTimer from PySide6.QtGui import QPicture, QPainter from PySide6.QtWidgets import QWidget @@ -43,8 +43,6 @@ class QPictureTest(UsesQApplication): w = MyWidget(picture2) w.show() - while not w.windowHandle().isExposed(): - QCoreApplication.processEvents() self.app.exec() diff --git a/sources/pyside6/tests/QtWidgets/qwidget_test.py b/sources/pyside6/tests/QtWidgets/qwidget_test.py index 28c189e1..c1ff43b5 100644 --- a/sources/pyside6/tests/QtWidgets/qwidget_test.py +++ b/sources/pyside6/tests/QtWidgets/qwidget_test.py @@ -12,6 +12,7 @@ from init_paths import init_test_paths init_test_paths(False) from PySide6.QtWidgets import QWidget, QMainWindow +from PySide6.QtGui import QGuiApplication from helper.usesqapplication import UsesQApplication @@ -47,10 +48,10 @@ class QWidgetVisible(UsesQApplication): widget.setVisible(True) self.assertTrue(widget.isVisible()) self.assertTrue(widget.winId() != 0) - # skip this test on macOS since no native events are received - if sys.platform == 'darwin': + # skip this test on macOS/Wayland since no native events are received + if sys.platform == 'darwin' or QGuiApplication.platformName() == 'wayland': return - for i in range(10): + for i in range(100): if widget.nativeEventCount > 0: break self.app.processEvents() diff --git a/sources/pyside6/tests/QtWidgets/reference_count_test.py b/sources/pyside6/tests/QtWidgets/reference_count_test.py index 67a39e12..65cb8ec8 100644 --- a/sources/pyside6/tests/QtWidgets/reference_count_test.py +++ b/sources/pyside6/tests/QtWidgets/reference_count_test.py @@ -51,7 +51,8 @@ class ReferenceCount(UsesQApplication): self.wrp = weakref.ref(pol, pol_del) # refcount need be 3 because one ref for QGraphicsScene, and one to rect obj - self.assertEqual(sys.getrefcount(pol), 3) + expected_ref_count = 2 if sys.version_info >= (3, 14) else 3 + self.assertEqual(sys.getrefcount(pol), expected_ref_count) @unittest.skipUnless(hasattr(sys, "getrefcount"), f"{sys.implementation.name} has no refcount") def testReferenceCount(self): @@ -66,7 +67,8 @@ class ReferenceCount(UsesQApplication): self.wrr = weakref.ref(rect, rect_del) # refcount need be 3 because one ref for QGraphicsScene, and one to rect obj - self.assertEqual(sys.getrefcount(rect), 3) + expected_ref_count = 2 if sys.version_info >= (3, 14) else 3 + self.assertEqual(sys.getrefcount(rect), expected_ref_count) del rect # not destroyed because one ref continue in QGraphicsScene diff --git a/sources/pyside6/tests/QtXml/qdomdocument_test.py b/sources/pyside6/tests/QtXml/qdomdocument_test.py index 8fe4f6e1..b321b1bd 100644 --- a/sources/pyside6/tests/QtXml/qdomdocument_test.py +++ b/sources/pyside6/tests/QtXml/qdomdocument_test.py @@ -44,18 +44,20 @@ class QDomDocumentTest(unittest.TestCase): def testQDomDocumentSetContentWithBadXmlData(self): '''Sets invalid xml as the QDomDocument contents.''' - ok, errorStr, errorLine, errorColumn = self.dom.setContent(self.badXmlData, True) - self.assertFalse(ok) - self.assertEqual(errorStr, 'Opening and ending tag mismatch.') - self.assertEqual(errorLine, 4) + parseResult = self.dom.setContent(self.badXmlData, + QDomDocument.ParseOption.UseNamespaceProcessing) + self.assertFalse(parseResult) + self.assertEqual(parseResult.errorMessage, 'Opening and ending tag mismatch.') + self.assertEqual(parseResult.errorLine, 4) def testQDomDocumentSetContentWithGoodXmlData(self): '''Sets valid xml as the QDomDocument contents.''' - ok, errorStr, errorLine, errorColumn = self.dom.setContent(self.goodXmlData, True) - self.assertTrue(ok) - self.assertEqual(errorStr, '') - self.assertEqual(errorLine, 0) - self.assertEqual(errorColumn, 0) + parseResult = self.dom.setContent(self.goodXmlData, + QDomDocument.ParseOption.UseNamespaceProcessing) + self.assertTrue(parseResult) + self.assertEqual(parseResult.errorMessage, '') + self.assertEqual(parseResult.errorLine, 0) + self.assertEqual(parseResult.errorColumn, 0) def testQDomDocumentData(self): '''Checks the QDomDocument elements for the valid xml contents.''' @@ -66,7 +68,8 @@ class QDomDocumentTest(unittest.TestCase): self.assertTrue(element.hasAttribute(attribute)) self.assertEqual(element.attribute(attribute), value) - ok, errorStr, errorLine, errorColumn = self.dom.setContent(self.goodXmlData, True) + parseResult = self.dom.setContent(self.goodXmlData, # noqa F:841 + QDomDocument.ParseOption.UseNamespaceProcessing) root = self.dom.documentElement() self.assertEqual(root.tagName(), 'typesystem') checkAttribute(root, 'package', 'PySide6.QtXml') diff --git a/sources/pyside6/tests/pysidetest/CMakeLists.txt b/sources/pyside6/tests/pysidetest/CMakeLists.txt index 4a7e2e1d..8b4de5d8 100644 --- a/sources/pyside6/tests/pysidetest/CMakeLists.txt +++ b/sources/pyside6/tests/pysidetest/CMakeLists.txt @@ -156,7 +156,6 @@ PYSIDE_TEST(new_inherited_functions_test.py) PYSIDE_TEST(notify_id.py) PYSIDE_TEST(properties_test.py) PYSIDE_TEST(property_python_test.py) -PYSIDE_TEST(snake_case_sub.py) PYSIDE_TEST(snake_case_test.py) PYSIDE_TEST(true_property_test.py) PYSIDE_TEST(qapp_like_a_macro_test.py) diff --git a/sources/pyside6/tests/pysidetest/enum_test.py b/sources/pyside6/tests/pysidetest/enum_test.py index ab20cbab..a60b4d18 100644 --- a/sources/pyside6/tests/pysidetest/enum_test.py +++ b/sources/pyside6/tests/pysidetest/enum_test.py @@ -20,8 +20,8 @@ import dis class ListConnectionTest(unittest.TestCase): def testEnumVisibility(self): - self.assertEqual(Enum1.Option1, 1) - self.assertEqual(Enum1.Option2, 2) + self.assertEqual(Enum1.Option1.value, 1) + self.assertEqual(Enum1.Option2.value, 2) self.assertEqual(TestObjectWithoutNamespace.Enum2.Option3, 3) self.assertEqual(TestObjectWithoutNamespace.Enum2.Option4, 4) diff --git a/sources/pyside6/tests/pysidetest/mypy_correctness_test.py b/sources/pyside6/tests/pysidetest/mypy_correctness_test.py index 83d80efc..9d836cfe 100644 --- a/sources/pyside6/tests/pysidetest/mypy_correctness_test.py +++ b/sources/pyside6/tests/pysidetest/mypy_correctness_test.py @@ -69,8 +69,9 @@ class MypyCorrectnessTest(unittest.TestCase): def testMypy(self): self.assertTrue(HAVE_MYPY) insert_version = ["--python-version", "3.11"] if sys.version_info[:2] < (3, 11) else [] + exclusion = ["--exclude", "QtAsyncio"] cmd = ([sys.executable, "-m", "mypy", "--pretty", "--cache-dir", self.cache_dir] - + insert_version + [self.pyside_dir]) + + exclusion + insert_version + [self.pyside_dir]) time_pre = time.time() ret = subprocess.run(cmd, capture_output=True) time_post = time.time() diff --git a/sources/pyside6/tests/pysidetest/properties_test.py b/sources/pyside6/tests/pysidetest/properties_test.py index 00d7aad1..4452afe7 100644 --- a/sources/pyside6/tests/pysidetest/properties_test.py +++ b/sources/pyside6/tests/pysidetest/properties_test.py @@ -76,6 +76,40 @@ class TestDerivedObject(QStringListModel): notify=valueChanged) +class SpecialProperties(QObject): + _value = 1 + + def __init__(self): + super().__init__() + self._readWriteInt = 2 + self._readWriteDecoratedInt = 3 + + def readOnlyInt(self): # Class variable properties + return 4 + + def readWriteInt(self): + return self._readWriteInt + + def setReadWriteInt(self, v): + self._readWriteInt = v + + @Property(int) # Property decorators + def readOnlyDecoratedInt(self): + return 5 + + @Property(int) + def readWriteDecoratedInt(self): + return self._readWriteDecoratedInt + + @readWriteDecoratedInt.setter + def readWriteDecoratedInt(self, v): + self._readWriteDecoratedInt = v + + constantValue = Property(int, lambda self: self._value, constant=True) + readOnlyInt = Property(int, readOnlyInt) + readWriteInt = Property(int, readWriteInt, fset=setReadWriteInt) + + class PropertyTest(unittest.TestCase): def test1Object(self): @@ -104,6 +138,49 @@ class PropertyTest(unittest.TestCase): self.assertEqual(testObject.setter_called, 1) self.assertEqual(testObject.getter_called, 2) + def testSpecialProperties(self): + """PYSIDE-924, PYSIDE-3227, constant, read-only.""" + testObject = SpecialProperties() + mo = testObject.metaObject() + + i = mo.indexOfProperty("constantValue") + self.assertTrue(i != -1) + metaProperty = mo.property(i) + self.assertTrue(metaProperty.isConstant()) + self.assertEqual(testObject.constantValue, 1) + + i = mo.indexOfProperty("readWriteInt") + self.assertTrue(i != -1) + metaProperty = mo.property(i) + self.assertTrue(metaProperty.isWritable()) + self.assertEqual(testObject.readWriteInt, 2) + testObject.readWriteInt = 42 + self.assertEqual(testObject.readWriteInt, 42) + + i = mo.indexOfProperty("readWriteDecoratedInt") + self.assertTrue(i != -1) + metaProperty = mo.property(i) + self.assertTrue(metaProperty.isWritable()) + self.assertEqual(testObject.readWriteDecoratedInt, 3) + testObject.readWriteDecoratedInt = 42 + self.assertEqual(testObject.readWriteDecoratedInt, 42) + + i = mo.indexOfProperty("readOnlyInt") + self.assertTrue(i != -1) + metaProperty = mo.property(i) + self.assertFalse(metaProperty.isWritable()) + self.assertEqual(testObject.readOnlyInt, 4) + with self.assertRaises(AttributeError): + testObject.readOnlyInt = 42 + + i = mo.indexOfProperty("readOnlyDecoratedInt") + self.assertTrue(i != -1) + metaProperty = mo.property(i) + self.assertFalse(metaProperty.isWritable()) + self.assertEqual(testObject.readOnlyDecoratedInt, 5) + with self.assertRaises(AttributeError): + testObject.readOnlyDecoratedInt = 42 + if __name__ == '__main__': unittest.main() diff --git a/sources/pyside6/tests/pysidetest/qvariant_test.py b/sources/pyside6/tests/pysidetest/qvariant_test.py index 83b25b97..fee3b010 100644 --- a/sources/pyside6/tests/pysidetest/qvariant_test.py +++ b/sources/pyside6/tests/pysidetest/qvariant_test.py @@ -64,6 +64,9 @@ class QVariantTest(UsesQApplication): # check toInt() conversion for IntEnum self.assertEqual(PyTestQVariantEnum.getNumberFromQVarEnum(Qt.GestureType.TapGesture), 1) + # Test if enum still an enum on C++ land + self.assertTrue(TestQVariantEnum.isQtOrientationEnum(Qt.Orientation.Vertical)) + if __name__ == '__main__': unittest.main() diff --git a/sources/pyside6/tests/pysidetest/snake_case_imported.py b/sources/pyside6/tests/pysidetest/snake_case_imported.py new file mode 100644 index 00000000..c79966e1 --- /dev/null +++ b/sources/pyside6/tests/pysidetest/snake_case_imported.py @@ -0,0 +1,25 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +from __future__ import annotations + +import os +import sys + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths # noqa: E402 +init_test_paths(False) + +from __feature__ import snake_case # noqa + +""" +PYSIDE-3250: Tests that snake_case works when used in several files +""" + +from PySide6.QtWidgets import QWidget # noqa: E402 + + +def test(): + print(__name__) + widget = QWidget() + return widget.size_hint() diff --git a/sources/pyside6/tests/pysidetest/snake_case_imported_no_snake_case.py b/sources/pyside6/tests/pysidetest/snake_case_imported_no_snake_case.py new file mode 100644 index 00000000..a5ce1469 --- /dev/null +++ b/sources/pyside6/tests/pysidetest/snake_case_imported_no_snake_case.py @@ -0,0 +1,23 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +from __future__ import annotations + +import os +import sys + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths # noqa: E402 +init_test_paths(False) + +""" +PYSIDE-2029: Tests that snake_case is isolated from imported modules +""" + +from PySide6.QtWidgets import QWidget # noqa: E402 + + +def test_no_snake_case(): + print(__name__) + widget = QWidget() + return widget.sizeHint() diff --git a/sources/pyside6/tests/pysidetest/snake_case_sub.py b/sources/pyside6/tests/pysidetest/snake_case_sub.py deleted file mode 100644 index e423542a..00000000 --- a/sources/pyside6/tests/pysidetest/snake_case_sub.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (C) 2022 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -from __future__ import annotations - -import os -import sys - -from pathlib import Path -sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) -from init_paths import init_test_paths # noqa: E402 -init_test_paths(False) - -""" -PYSIDE-2029: Tests that snake_case is isolated from imported modules -""" - -from PySide6.QtWidgets import QWidget # noqa: E402 - - -def test_no_snake_case(): - print(__name__) - widget = QWidget() - check = widget.sizeHint # noqa diff --git a/sources/pyside6/tests/pysidetest/snake_case_test.py b/sources/pyside6/tests/pysidetest/snake_case_test.py index bdcd996c..4667e584 100644 --- a/sources/pyside6/tests/pysidetest/snake_case_test.py +++ b/sources/pyside6/tests/pysidetest/snake_case_test.py @@ -21,18 +21,26 @@ if not is_pypy: from __feature__ import snake_case # noqa from helper.usesqapplication import UsesQApplication -import snake_case_sub +import snake_case_imported +import snake_case_imported_no_snake_case @unittest.skipIf(is_pypy, "__feature__ cannot yet be used with PyPy") class SnakeCaseNoPropagateTest(UsesQApplication): - def testSnakeCase(self): - # this worked + def testSnakeCaseImport(self): + """PYSIDE-3250: Test that snake case works when using it in imported modules.""" widget = QWidget() - check = widget.size_hint # noqa + r1 = widget.size_hint() + r2 = snake_case_imported.test() + self.assertEqual(r1, r2) - snake_case_sub.test_no_snake_case() + def testSnakeCaseImportNoSnakeCase(self): + """PYSIDE-2029: Tests that snake_case is isolated from imported modules.""" + widget = QWidget() + r1 = widget.size_hint() + r2 = snake_case_imported_no_snake_case.test_no_snake_case() + self.assertEqual(r1, r2) if __name__ == '__main__': diff --git a/sources/pyside6/tests/pysidetest/testqvariantenum.cpp b/sources/pyside6/tests/pysidetest/testqvariantenum.cpp index 7135e422..043073e8 100644 --- a/sources/pyside6/tests/pysidetest/testqvariantenum.cpp +++ b/sources/pyside6/tests/pysidetest/testqvariantenum.cpp @@ -27,3 +27,8 @@ bool TestQVariantEnum::isEnumChanneled() const { return this->channelingEnum(this->getRValEnum()); } + +bool TestQVariantEnum::isQtOrientationEnum(QVariant var) +{ + return (var.typeId() == QMetaType::fromType().id()); +} diff --git a/sources/pyside6/tests/pysidetest/testqvariantenum.h b/sources/pyside6/tests/pysidetest/testqvariantenum.h index 4b729e3d..d2923174 100644 --- a/sources/pyside6/tests/pysidetest/testqvariantenum.h +++ b/sources/pyside6/tests/pysidetest/testqvariantenum.h @@ -18,6 +18,8 @@ public: virtual QVariant getRValEnum() const; virtual bool channelingEnum(QVariant rvalEnum) const; virtual ~TestQVariantEnum() = default; + static bool isQtOrientationEnum(QVariant var); + private: QVariant m_enum; }; diff --git a/sources/pyside6/tests/signals/disconnect_test.py b/sources/pyside6/tests/signals/disconnect_test.py index 29ef312b..c94a2735 100644 --- a/sources/pyside6/tests/signals/disconnect_test.py +++ b/sources/pyside6/tests/signals/disconnect_test.py @@ -137,6 +137,20 @@ class TestDisconnect(unittest.TestCase): obj.signalWithDefaultValue.emit() self.assertTrue(self.called) + def testMultipleConnections(self): + """PYSIDE-3190: Signal.disconnect() should use the QMetaObject + code to disconnect, disconnecting multiple connections.""" + s = Sender() + r = Receiver() + s.bar.connect(r.receiver) + s.bar.connect(r.receiver) + s.bar.emit() + print(r.called) + self.assertEqual(r.called, 2) + s.bar.disconnect(r.receiver) + s.bar.emit() + self.assertEqual(r.called, 2) + if __name__ == '__main__': unittest.main() diff --git a/sources/pyside6/tests/signals/qobject_sender_test.py b/sources/pyside6/tests/signals/qobject_sender_test.py index 0e8ad2c0..12746698 100644 --- a/sources/pyside6/tests/signals/qobject_sender_test.py +++ b/sources/pyside6/tests/signals/qobject_sender_test.py @@ -14,9 +14,11 @@ sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) from init_paths import init_test_paths init_test_paths(False) -from PySide6.QtCore import QCoreApplication, QObject, QTimer, Signal +from PySide6.QtCore import QCoreApplication, QObject, QTimer, Signal, Slot from helper.usesqapplication import UsesQApplication +import samenamesender + class ExtQTimer(QTimer): def __init__(self): @@ -27,6 +29,30 @@ class Sender(QObject): foo = Signal() +class SameNameSender(samenamesender.SameNameSender): + ''' Test sender class for SameNameSenderTest (PYSIDE-3201).''' + signal3 = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self.signal1.connect(self.slot2) + self.signal2.connect(self.slot2) + self.signal3.connect(self.slot3) + self.slot3_invoked = False + + @Slot() + def slot1(self): + pass + + @Slot() + def slot2(self): + pass + + @Slot() + def slot3(self): + self.slot3_invoked = True + + class Receiver(QObject): def __init__(self): super().__init__() @@ -38,6 +64,15 @@ class Receiver(QObject): QCoreApplication.instance().exit() +class ResultReceiver(QObject): + def __init__(self): + super().__init__() + + @Slot(result=int) + def slotWithResult(self): + return 3 + + class ObjectSenderTest(unittest.TestCase): '''Test case for QObject.sender() method.''' @@ -114,5 +149,26 @@ class ObjectSenderWithQAppCheckOnReceiverTest(UsesQApplication): self.assertEqual(sender, recv.the_sender) +class SameNameSenderTest(UsesQApplication): + '''PYSIDE-3201: Test whether the meta object system is confused by identical + class names.''' + def test(self): + sender = SameNameSender() + sender.signal1.emit() + sender.signal2.emit() + sender.signal3.emit() + self.assertTrue(sender.slot3_invoked) + + +class ResultSlotTest(UsesQApplication): + '''PYSIDE-3266: Test that a slot declaring a result type does not cause crashes + in signal connections due to args[0] == 0.''' + def test(self): + sender = Sender() + recv = ResultReceiver() + sender.foo.connect(recv.slotWithResult) + sender.foo.emit() + + if __name__ == '__main__': unittest.main() diff --git a/sources/pyside6/tests/signals/samenamesender.py b/sources/pyside6/tests/signals/samenamesender.py new file mode 100644 index 00000000..269be996 --- /dev/null +++ b/sources/pyside6/tests/signals/samenamesender.py @@ -0,0 +1,13 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +from PySide6.QtCore import Signal, QObject + + +class SameNameSender(QObject): + ''' Base class for the Test sender class of SameNameSenderTest (PYSIDE-3201).''' + signal1 = Signal() + signal2 = Signal() + + def __init__(self, parent=None): + super().__init__(parent) diff --git a/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py b/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py index 648b07a5..a4d41275 100644 --- a/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py +++ b/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py @@ -330,11 +330,6 @@ class TestPySide6DeployQml(DeployTestBase): expected_modules = {"Core", "Gui", "Qml", "Quick", "Network", "OpenGL", "QmlModels", "QmlWorkerScript", "QmlMeta"} - # Exclude OpenGL if the platform is Windows and architecture is aarch64 - # https://bugreports.qt.io/browse/QTBUG-126030 - if sys.platform == "win32" and platform.machine() == "ARM64": - expected_modules.remove("OpenGL") - if sys.platform != "win32": expected_modules.add("DBus") obtained_modules = set(config_obj.get_value("qt", "modules").split(",")) @@ -395,6 +390,7 @@ class TestPySide6DeployWebEngine(DeployTestBase): "DownloadView.qml", "FindBar.qml", "FullScreenNotification.qml", + "WebAuthDialog.qml" ] data_files_cmd = " ".join( [ @@ -460,7 +456,7 @@ class TestPySide6DeployWebEngine(DeployTestBase): # Exclude specific modules if the platform is Windows and architecture is aarch64 if sys.platform == "win32" and platform.machine() == "ARM64": - excluded_modules = {"OpenGL", "WebEngineCore", "Positioning", "WebChannelQuick", + excluded_modules = {"WebEngineCore", "Positioning", "WebChannelQuick", "WebChannel"} expected_modules.difference_update(excluded_modules) diff --git a/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/pyproject.toml b/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/pyproject.toml index 1ceb0ac0..b6f16e69 100644 --- a/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/pyproject.toml +++ b/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/pyproject.toml @@ -2,4 +2,4 @@ name = "subproject" [tool.pyside6-project] -files = ["subproject_button.py"] +files = ["subproject_button.py", "../main.py"] diff --git a/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/subproject.pyproject b/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/subproject.pyproject index abfa1f46..688f07c3 100644 --- a/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/subproject.pyproject +++ b/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/subproject.pyproject @@ -1,3 +1,3 @@ { - "files": ["subproject_button.py"] + "files": ["subproject_button.py", "../main.py"] } diff --git a/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/expected_pyproject.toml b/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/expected_pyproject.toml index be8aa949..c0adb0c7 100644 --- a/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/expected_pyproject.toml +++ b/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/expected_pyproject.toml @@ -15,8 +15,9 @@ target-version = ["py38"] # Another comment -[tool.pyside6-project] -files = ["main.py", "zzz.py"] [build-system] requires = ["setuptools >=42"] build-backend = "setuptools.build_meta" + +[tool.pyside6-project] +files = ["main.py", "zzz.py"] diff --git a/sources/pyside6/tests/tools/pyside6-project/test_pyside6_project.py b/sources/pyside6/tests/tools/pyside6-project/test_pyside6_project.py index d6639525..cbacc4e4 100644 --- a/sources/pyside6/tests/tools/pyside6-project/test_pyside6_project.py +++ b/sources/pyside6/tests/tools/pyside6-project/test_pyside6_project.py @@ -1,5 +1,7 @@ # Copyright (C) 2024 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +from __future__ import annotations + import contextlib import difflib import importlib diff --git a/sources/shiboken6/.cmake.conf b/sources/shiboken6/.cmake.conf index 248b66f5..7bd76c7c 100644 --- a/sources/shiboken6/.cmake.conf +++ b/sources/shiboken6/.cmake.conf @@ -1,5 +1,5 @@ set(shiboken_MAJOR_VERSION "6") -set(shiboken_MINOR_VERSION "9") +set(shiboken_MINOR_VERSION "10") set(shiboken_MICRO_VERSION "2") set(shiboken_PRE_RELEASE_VERSION_TYPE "") set(shiboken_PRE_RELEASE_VERSION "") diff --git a/sources/shiboken6/ApiExtractor/abstractmetabuilder.cpp b/sources/shiboken6/ApiExtractor/abstractmetabuilder.cpp index a9c61fdd..9807e1ea 100644 --- a/sources/shiboken6/ApiExtractor/abstractmetabuilder.cpp +++ b/sources/shiboken6/ApiExtractor/abstractmetabuilder.cpp @@ -453,12 +453,29 @@ FileModelItem AbstractMetaBuilderPrivate::buildDom(QByteArrayList arguments, unsigned clangFlags) { clang::Builder builder; + clang::setHeuristicOptions(arguments); builder.setForceProcessSystemIncludes(TypeDatabase::instance()->forceProcessSystemIncludes()); if (addCompilerSupportArguments) { if (level == LanguageLevel::Default) level = clang::emulatedCompilerLanguageLevel(); arguments.prepend(QByteArrayLiteral("-std=") + clang::languageLevelOption(level)); + // Add target for qsystemdetection.h to set the right Q_OS_ definitions + if (clang::isCrossCompilation() && !clang::hasTargetOption(arguments)) { + const auto triplet = clang::targetTripletForPlatform(clang::platform(), + clang::architecture(), + clang::compiler(), + clang::platformVersion()); + if (triplet.isEmpty()) { + qCWarning(lcShiboken, + "Unable to determine a cross compilation target triplet (%d/%d/%d).", + int(clang::platform()), int(clang::architecture()), int(clang::compiler())); + } else { + arguments.prepend("--target="_ba + triplet); + const auto msg = "Setting clang target: "_L1 + QLatin1StringView(triplet); + ReportHandler::addGeneralMessage(msg); + } + } } FileModelItem result = clang::parse(arguments, addCompilerSupportArguments, level, clangFlags, builder) @@ -514,13 +531,22 @@ void AbstractMetaBuilderPrivate::traverseDom(const FileModelItem &dom, ReportHandler::startProgress("Generated enum model (" + QByteArray::number(enums.size()) + ")."); for (const EnumModelItem &item : enums) { - auto metaEnum = traverseEnum(item, nullptr, QSet()); + auto metaEnum = traverseEnum(item, nullptr); if (metaEnum.has_value()) { if (metaEnum->typeEntry()->generateCode()) m_globalEnums << metaEnum.value(); } } + const auto &globalTypeDefs = dom->typeDefs(); + for (const auto &typeDef : globalTypeDefs) { + if (typeDef->underlyingTypeCategory() == TypeCategory::Enum) { + const auto metaEnum = traverseTypedefedEnum(dom, typeDef, {}); + if (metaEnum.has_value()) + m_globalEnums.append(metaEnum.value()); + } + } + const auto &namespaceTypeValues = dom->namespaces(); ReportHandler::startProgress("Generated namespace model (" + QByteArray::number(namespaceTypeValues.size()) + ")."); @@ -748,7 +774,7 @@ AbstractMetaClassPtr AbstractMetaBuilderPrivate::traverseNamespace(const FileModelItem &dom, const NamespaceModelItem &namespaceItem) { - QString namespaceName = currentScope()->qualifiedName().join(u"::"_s); + QString namespaceName = currentScope()->qualifiedNameString(); if (!namespaceName.isEmpty()) namespaceName.append(u"::"_s); namespaceName.append(namespaceItem->name()); @@ -792,7 +818,7 @@ AbstractMetaClassPtr m_itemToClass.insert(namespaceItem.get(), metaClass); } - traverseEnums(namespaceItem, metaClass, namespaceItem->enumsDeclarations()); + traverseEnums(namespaceItem, metaClass); pushScope(namespaceItem); @@ -810,11 +836,20 @@ AbstractMetaClassPtr // specific typedefs to be used as classes. const TypeDefList typeDefs = namespaceItem->typeDefs(); for (const TypeDefModelItem &typeDef : typeDefs) { - const auto cls = traverseTypeDef(dom, typeDef, metaClass); - if (cls) { - metaClass->addInnerClass(cls); - cls->setEnclosingClass(metaClass); - addAbstractMetaClass(cls, typeDef.get()); + switch (typeDef->underlyingTypeCategory()) { + case TypeCategory::Enum: { + const auto metaEnum = traverseTypedefedEnum(dom, typeDef, metaClass); + if (metaEnum.has_value()) + metaClass->addEnum(metaEnum.value()); + } + break; + default: + if (const auto cls = traverseTypeDef(dom, typeDef, metaClass)) { + metaClass->addInnerClass(cls); + cls->setEnclosingClass(metaClass); + addAbstractMetaClass(cls, typeDef.get()); + } + break; } } @@ -839,16 +874,15 @@ AbstractMetaClassPtr std::optional AbstractMetaBuilderPrivate::traverseEnum(const EnumModelItem &enumItem, - const AbstractMetaClassPtr &enclosing, - const QSet &enumsDeclarations) + const AbstractMetaClassPtr &enclosing) { - QString qualifiedName = enumItem->qualifiedName().join(u"::"_s); + QString qualifiedName = enumItem->qualifiedNameString(); TypeEntryPtr typeEntry; - const auto enclosingTypeEntry = enclosing ? enclosing->typeEntry() : TypeEntryCPtr{}; if (enumItem->accessPolicy() == Access::Private) { + Q_ASSERT(enclosing); typeEntry = std::make_shared(enumItem->qualifiedName().constLast(), - QVersionNumber(0, 0), enclosingTypeEntry); + QVersionNumber(0, 0), enclosing->typeEntry()); TypeDatabase::instance()->addType(typeEntry); } else if (enumItem->enumKind() != AnonymousEnum) { typeEntry = TypeDatabase::instance()->findType(qualifiedName); @@ -864,12 +898,17 @@ std::optional break; } } + return createMetaEnum(enumItem, qualifiedName, typeEntry, enclosing); +} - QString enumName = enumItem->name(); - - QString className; - if (enclosingTypeEntry) - className = enclosingTypeEntry->qualifiedCppName(); +std::optional + AbstractMetaBuilderPrivate::createMetaEnum(const EnumModelItem &enumItem, + const QString &qualifiedName, + const TypeEntryPtr &typeEntry, + const AbstractMetaClassPtr &enclosing) +{ + const QString enumName = enumItem->name(); + const QString className = enclosing ? enclosing->typeEntry()->qualifiedCppName() : QString{}; QString rejectReason; if (TypeDatabase::instance()->isEnumRejected(className, enumName, &rejectReason)) { @@ -905,10 +944,6 @@ std::optional metaEnum.setDeprecated(enumItem->isDeprecated()); metaEnum.setUnderlyingType(enumItem->underlyingType()); metaEnum.setSigned(enumItem->isSigned()); - if (enumsDeclarations.contains(qualifiedName) - || enumsDeclarations.contains(enumName)) { - metaEnum.setHasQEnumsDeclaration(true); - } auto enumTypeEntry = std::static_pointer_cast(typeEntry); metaEnum.setTypeEntry(enumTypeEntry); @@ -955,6 +990,49 @@ std::optional return metaEnum; } +// Add typedef'ed enumerations ("Using MyEnum=SomeNamespace::MyEnum") for which +// a type entry exists. +std::optional + AbstractMetaBuilderPrivate::traverseTypedefedEnum(const FileModelItem &dom, + const TypeDefModelItem &typeDefItem, + const AbstractMetaClassPtr &enclosing) +{ + if (enclosing && typeDefItem->accessPolicy() != Access::Public) + return std::nullopt; // Only for global/public enums typedef'ed into classes/namespaces + auto modelItem = CodeModel::findItem(typeDefItem->type().qualifiedName(), dom); + if (!modelItem || modelItem->kind() != _CodeModelItem::Kind_Enum) + return std::nullopt; + auto enumItem = std::static_pointer_cast<_EnumModelItem>(modelItem); + if (enumItem->accessPolicy() != Access::Public) + return std::nullopt; + // Name in class + QString qualifiedName = enclosing + ? enclosing->qualifiedCppName() + "::"_L1 + typeDefItem->name() : typeDefItem->name(); + auto targetTypeEntry = TypeDatabase::instance()->findType(qualifiedName); + if (!targetTypeEntry || !targetTypeEntry->isEnum() || !targetTypeEntry->generateCode()) + return std::nullopt; + auto targetEnumTypeEntry = std::static_pointer_cast(targetTypeEntry); + auto sourceTypeEntry = TypeDatabase::instance()->findType(enumItem->qualifiedNameString()); + if (!sourceTypeEntry || !sourceTypeEntry->isEnum()) + return std::nullopt; + + auto sourceEnumTypeEntry = std::static_pointer_cast(sourceTypeEntry); + if (sourceEnumTypeEntry == targetEnumTypeEntry) // Reject "typedef Enum1 { V1 } Enum1;" + return std::nullopt; + + const QString message = "Enum \""_L1 + qualifiedName + "\" is an alias to \""_L1 + + enumItem->qualifiedNameString() + "\"."_L1; + ReportHandler::addGeneralMessage(message); + auto result = createMetaEnum(enumItem, qualifiedName, targetTypeEntry, enclosing); + if (result.has_value()) { + targetEnumTypeEntry->setAliasMode(EnumTypeEntry::AliasTarget); + targetEnumTypeEntry->setAliasTypeEntry(sourceEnumTypeEntry); + sourceEnumTypeEntry->setAliasMode(EnumTypeEntry::AliasSource); + sourceEnumTypeEntry->setAliasTypeEntry(targetEnumTypeEntry); + } + return result; +} + AbstractMetaClassPtr AbstractMetaBuilderPrivate::traverseTypeDef(const FileModelItem &dom, const TypeDefModelItem &typeDef, @@ -1160,7 +1238,7 @@ AbstractMetaClassPtr AbstractMetaBuilderPrivate::traverseClass(const FileModelIt parseQ_Properties(metaClass, classItem->propertyDeclarations()); - traverseEnums(classItem, metaClass, classItem->enumsDeclarations()); + traverseEnums(classItem, metaClass); // Inner classes { @@ -1180,10 +1258,20 @@ AbstractMetaClassPtr AbstractMetaBuilderPrivate::traverseClass(const FileModelIt // specific typedefs to be used as classes. const TypeDefList typeDefs = classItem->typeDefs(); for (const TypeDefModelItem &typeDef : typeDefs) { - const auto cls = traverseTypeDef(dom, typeDef, metaClass); - if (cls) { - cls->setEnclosingClass(metaClass); - addAbstractMetaClass(cls, typeDef.get()); + if (typeDef->accessPolicy() != Access::Private) { + switch (typeDef->underlyingTypeCategory()) { + case TypeCategory::Enum: { + const auto metaEnum = traverseTypedefedEnum(dom, typeDef, metaClass); + if (metaEnum.has_value()) + metaClass->addEnum(metaEnum.value()); + } + break; + default: + if (const auto cls = traverseTypeDef(dom, typeDef, metaClass)) { + cls->setEnclosingClass(metaClass); + addAbstractMetaClass(cls, typeDef.get()); + } + } } } @@ -1631,13 +1719,11 @@ bool AbstractMetaBuilderPrivate::setupInheritance(const AbstractMetaClassPtr &me } void AbstractMetaBuilderPrivate::traverseEnums(const ScopeModelItem &scopeItem, - const AbstractMetaClassPtr &metaClass, - const QStringList &enumsDeclarations) + const AbstractMetaClassPtr &metaClass) { const EnumList &enums = scopeItem->enums(); - const QSet enumsDeclarationSet(enumsDeclarations.cbegin(), enumsDeclarations.cend()); for (const EnumModelItem &enumItem : enums) { - auto metaEnum = traverseEnum(enumItem, metaClass, enumsDeclarationSet); + auto metaEnum = traverseEnum(enumItem, metaClass); if (metaEnum.has_value()) { metaClass->addEnum(metaEnum.value()); } @@ -1696,10 +1782,7 @@ AbstractMetaFunctionPtr const auto &args = addedFunc->arguments(); - qsizetype argCount = args.size(); - // Check "foo(void)" - if (argCount == 1 && args.constFirst().typeInfo.isVoid()) - argCount = 0; + const qsizetype argCount = args.size(); for (qsizetype i = 0; i < argCount; ++i) { const AddedFunction::Argument &arg = args.at(i); auto type = translateType(arg.typeInfo, metaClass, {}, errorMessage); @@ -2771,6 +2854,16 @@ std::optional TypeEntryCList types = findTypeEntries(qualifiedName, name, flags, currentClass, d, errorMessageIn); + if (types.isEmpty() && !typeInfo.instantiations().isEmpty()) { + // Allow for specifying template specializations as primitive types + // with converters ('std::optional' or similar). + auto pt = TypeDatabase::instance()->findPrimitiveType(typeInfo.qualifiedInstantationName()); + if (pt) { + types.append(pt); + typeInfo.clearInstantiations(); + } + } + if (!flags.testFlag(AbstractMetaBuilder::TemplateArgument)) { // Avoid clashes between QByteArray and enum value QMetaType::QByteArray // unless we are looking for template arguments. diff --git a/sources/shiboken6/ApiExtractor/abstractmetabuilder_p.h b/sources/shiboken6/ApiExtractor/abstractmetabuilder_p.h index 45dd21e0..0a09d578 100644 --- a/sources/shiboken6/ApiExtractor/abstractmetabuilder_p.h +++ b/sources/shiboken6/ApiExtractor/abstractmetabuilder_p.h @@ -94,10 +94,14 @@ public: AbstractMetaClassPtr traverseNamespace(const FileModelItem &dom, const NamespaceModelItem &item); std::optional traverseEnum(const EnumModelItem &item, - const AbstractMetaClassPtr &enclosing, - const QSet &enumsDeclarations); - void traverseEnums(const ScopeModelItem &item, const AbstractMetaClassPtr &parent, - const QStringList &enumsDeclarations); + const AbstractMetaClassPtr &enclosing); + void traverseEnums(const ScopeModelItem &item, const AbstractMetaClassPtr &parent); + std::optional + createMetaEnum(const EnumModelItem &enumItem, const QString &qualifiedName, + const TypeEntryPtr &typeEntry, const AbstractMetaClassPtr &enclosing); + std::optional + traverseTypedefedEnum(const FileModelItem &dom, const TypeDefModelItem &typeDefItem, + const AbstractMetaClassPtr &enclosing); AbstractMetaFunctionList classFunctionList(const ScopeModelItem &scopeItem, AbstractMetaClass::Attributes *constructorAttributes, const AbstractMetaClassPtr ¤tClass); diff --git a/sources/shiboken6/ApiExtractor/abstractmetaenum.cpp b/sources/shiboken6/ApiExtractor/abstractmetaenum.cpp index 486d3d39..3f2714ff 100644 --- a/sources/shiboken6/ApiExtractor/abstractmetaenum.cpp +++ b/sources/shiboken6/ApiExtractor/abstractmetaenum.cpp @@ -96,8 +96,7 @@ void AbstractMetaEnumValue::setDocumentation(const Documentation &doc) class AbstractMetaEnumData : public QSharedData { public: - AbstractMetaEnumData() : m_deprecated(false), - m_hasQenumsDeclaration(false), m_signed(true) + AbstractMetaEnumData() : m_deprecated(false), m_signed(true) { } @@ -113,7 +112,6 @@ public: EnumKind m_enumKind = CEnum; Access m_access = Access::Public; uint m_deprecated : 1; - uint m_hasQenumsDeclaration : 1; uint m_signed : 1; }; @@ -298,17 +296,6 @@ bool AbstractMetaEnum::isAnonymous() const return d->m_enumKind == AnonymousEnum; } -bool AbstractMetaEnum::hasQEnumsDeclaration() const -{ - return d->m_hasQenumsDeclaration; -} - -void AbstractMetaEnum::setHasQEnumsDeclaration(bool on) -{ - if (d->m_hasQenumsDeclaration != on) - d->m_hasQenumsDeclaration = on; -} - EnumTypeEntryCPtr AbstractMetaEnum::typeEntry() const { return d->m_typeEntry; diff --git a/sources/shiboken6/ApiExtractor/abstractmetaenum.h b/sources/shiboken6/ApiExtractor/abstractmetaenum.h index cfaa9b9b..c7edb74f 100644 --- a/sources/shiboken6/ApiExtractor/abstractmetaenum.h +++ b/sources/shiboken6/ApiExtractor/abstractmetaenum.h @@ -96,10 +96,6 @@ public: bool isAnonymous() const; - // Has the enum been declared inside a Q_ENUMS() macro in its enclosing class? - bool hasQEnumsDeclaration() const; - void setHasQEnumsDeclaration(bool on); - EnumTypeEntryCPtr typeEntry() const; void setTypeEntry(const EnumTypeEntryCPtr &entry); diff --git a/sources/shiboken6/ApiExtractor/abstractmetalang.cpp b/sources/shiboken6/ApiExtractor/abstractmetalang.cpp index e88f354b..107d89d5 100644 --- a/sources/shiboken6/ApiExtractor/abstractmetalang.cpp +++ b/sources/shiboken6/ApiExtractor/abstractmetalang.cpp @@ -172,7 +172,7 @@ bool AbstractMetaClass::isPolymorphic() const AbstractMetaFunctionCList AbstractMetaClass::queryFunctionsByName(const QString &name) const { AbstractMetaFunctionCList returned; - for (const auto &function : d->m_functions) { + for (const auto &function : std::as_const(d->m_functions)) { if (function->name() == name) returned.append(function); } @@ -434,7 +434,7 @@ bool AbstractMetaClass::hasSignal(const AbstractMetaFunction *other) const if (!other->isSignal()) return false; - for (const auto &f : d->m_functions) { + for (const auto &f : std::as_const(d->m_functions)) { if (f->isSignal() && f->compareTo(other) & AbstractMetaFunction::EqualName) return other->modifiedName() == f->modifiedName(); } @@ -1395,7 +1395,7 @@ void AbstractMetaClass::addEnum(const AbstractMetaEnum &e) std::optional AbstractMetaClass::findEnum(const QString &enumName) const { - for (const auto &e : d->m_enums) { + for (const auto &e : std::as_const(d->m_enums)) { if (e.name() == enumName) return e; } @@ -1421,7 +1421,7 @@ std::optional void AbstractMetaClass::getEnumsToBeGenerated(AbstractMetaEnumList *enumList) const { - for (const AbstractMetaEnum &metaEnum : d->m_enums) { + for (const AbstractMetaEnum &metaEnum : std::as_const(d->m_enums)) { if (!metaEnum.isPrivate() && metaEnum.typeEntry()->generateCode()) enumList->append(metaEnum); } @@ -1496,7 +1496,7 @@ void AbstractMetaClassPrivate::addUsingConstructors(const AbstractMetaClassPtr & return; } - for (const auto &superClass : m_baseClasses) { + for (const auto &superClass : std::as_const(m_baseClasses)) { // Find any "using base-constructor" directives if (isUsingMember(superClass, superClass->name(), Access::Protected)) { // Add to derived class with parameter lists. @@ -1538,7 +1538,7 @@ void AbstractMetaClass::fixFunctions(const AbstractMetaClassPtr &klass, bool avo nonRemovedFuncs.append(f); } - for (const auto &superClassC : d->m_baseClasses) { + for (const auto &superClassC : std::as_const(d->m_baseClasses)) { for (const auto &pof : superClassC->userAddedPythonOverrides()) { auto *clonedPof = pof->copy(); clonedPof->setOwnerClass(klass); @@ -1941,7 +1941,7 @@ void AbstractMetaClass::format(QDebug &debug) const debug << " [deleted move assignment]"; if (!d->m_baseClasses.isEmpty()) { debug << ", inherits "; - for (const auto &b : d->m_baseClasses) + for (const auto &b : std::as_const(d->m_baseClasses)) debug << " \"" << b->name() << '"'; } diff --git a/sources/shiboken6/ApiExtractor/addedfunction.cpp b/sources/shiboken6/ApiExtractor/addedfunction.cpp index ee8009cf..64929606 100644 --- a/sources/shiboken6/ApiExtractor/addedfunction.cpp +++ b/sources/shiboken6/ApiExtractor/addedfunction.cpp @@ -165,10 +165,12 @@ AddedFunction::AddedFunctionPtr } const auto paramString = signature.mid(openParenPos + 1, closingParenPos - openParenPos - 1); - const auto params = AddedFunctionParser::splitParameters(paramString, errorMessage); + auto params = AddedFunctionParser::splitParameters(paramString, errorMessage); if (params.isEmpty() && !errorMessage->isEmpty()) return {}; - for (const auto &p : params) { + if (params.size() == 1 && params.constFirst().type == "void"_L1) + params.clear(); // "void foo(void)" -> ""void foo()" + for (const auto &p : std::as_const(params)) { TypeInfo type = p.type == u"..." ? TypeInfo::varArgsType() : TypeParser::parse(p.type, errorMessage); if (!errorMessage->isEmpty()) { diff --git a/sources/shiboken6/ApiExtractor/apiextractor.cpp b/sources/shiboken6/ApiExtractor/apiextractor.cpp index 38dedfd1..1c398313 100644 --- a/sources/shiboken6/ApiExtractor/apiextractor.cpp +++ b/sources/shiboken6/ApiExtractor/apiextractor.cpp @@ -370,7 +370,6 @@ bool ApiExtractorPrivate::runHelper(ApiExtractorFlags flags) bool addCompilerSupportArguments = true; if (clangOptionsSize > 0) { - clang::setTargetTriple(m_clangOptions); qsizetype i = 0; if (m_clangOptions.at(i) == u"-") { ++i; @@ -614,8 +613,8 @@ ApiExtractorPrivate::addInstantiatedContainersAndSmartPointers(InstantiationColl if (type.hasTemplateChildren()) { const auto piece = isContainer ? "container"_L1 : "smart pointer"_L1; QString warning = - QString::fromLatin1("Skipping instantiation of %1 '%2' because it has template" - " arguments.").arg(piece, type.originalTypeDescription()); + "Skipping instantiation of %1 '%2' because it has template" + " arguments."_L1.arg(piece, type.originalTypeDescription()); if (!contextName.isEmpty()) warning.append(" Calling context: "_L1 + contextName); diff --git a/sources/shiboken6/ApiExtractor/clangparser/clangbuilder.cpp b/sources/shiboken6/ApiExtractor/clangparser/clangbuilder.cpp index 5188262d..cc292428 100644 --- a/sources/shiboken6/ApiExtractor/clangparser/clangbuilder.cpp +++ b/sources/shiboken6/ApiExtractor/clangparser/clangbuilder.cpp @@ -524,6 +524,27 @@ void BuilderPrivate::addTemplateInstantiations(const CXType &type, typeName->remove(pos.first, pos.second - pos.first); } +static TypeCategory typeCategoryFromClang(CXTypeKind k) +{ + switch (k) { + case CXType_Void: + return TypeCategory::Void; + case CXType_Enum: + return TypeCategory::Enum; + case CXType_Pointer: + case CXType_BlockPointer: + return TypeCategory::Pointer; + case CXType_FunctionNoProto: + case CXType_FunctionProto: + return TypeCategory::Function; + default: + break; + } + if (k >= CXType_FirstBuiltin && k <= CXType_LastBuiltin) + return TypeCategory::Builtin; + return TypeCategory::Other; +} + TypeInfo BuilderPrivate::createTypeInfoUncached(const CXType &type, bool *cacheable) const { @@ -533,6 +554,7 @@ TypeInfo BuilderPrivate::createTypeInfoUncached(const CXType &type, if (argCount >= 0) { TypeInfo result = createTypeInfoUncached(clang_getResultType(pointeeType), cacheable); + result.setTypeCategory(TypeCategory::Pointer); result.setFunctionPointer(true); for (int a = 0; a < argCount; ++a) result.addArgument(createTypeInfoUncached(clang_getArgType(pointeeType, unsigned(a)), @@ -542,6 +564,7 @@ TypeInfo BuilderPrivate::createTypeInfoUncached(const CXType &type, } TypeInfo typeInfo; + typeInfo.setTypeCategory(typeCategoryFromClang(clang_getCanonicalType(type).kind)); CXType nestedType = type; for (; isArrayType(nestedType.kind); nestedType = clang_getArrayElementType(nestedType)) { @@ -623,6 +646,7 @@ void BuilderPrivate::addTypeDef(const CXCursor &cursor, const CXType &cxType) setFileName(cursor, item.get()); item->setType(createTypeInfo(cxType)); item->setScope(m_scope); + item->setAccessPolicy(accessPolicy(clang_getCXXAccessSpecifier(cursor))); m_scopeStack.back()->addTypeDef(item); } @@ -827,6 +851,7 @@ BuilderPrivate::SpecialSystemHeader } switch (clang::platform()) { + case Platform::Linux: case Platform::Unix: if (fileName == u"/usr/include/stdlib.h" || baseName == u"types.h" @@ -836,6 +861,7 @@ BuilderPrivate::SpecialSystemHeader } break; case Platform::macOS: + case Platform::iOS: // Parse the following system headers to get the correct typdefs for types like // int32_t, which are used in the macOS implementation of OpenGL framework. // They are installed under /Applications/Xcode.app/Contents/Developer/Platforms... diff --git a/sources/shiboken6/ApiExtractor/clangparser/clangparser.cpp b/sources/shiboken6/ApiExtractor/clangparser/clangparser.cpp index 747937ed..4c63c3e8 100644 --- a/sources/shiboken6/ApiExtractor/clangparser/clangparser.cpp +++ b/sources/shiboken6/ApiExtractor/clangparser/clangparser.cpp @@ -249,6 +249,11 @@ static CXTranslationUnit createTranslationUnit(CXIndex index, // https://github.com/darlinghq/darling/issues/204 #endif "-Wno-constant-logical-operand", +#if CINDEX_VERSION_MAJOR > 0 || CINDEX_VERSION_MINOR >= 64 // Clang 21 + // QTBUG-141204: Suppress "character-conversion" warnings in Qt: qchar.h:... warning: implicit + // conversion from 'const char16_t' to 'char32_t' may change the meaning of the represented code unit. + "-Wno-character-conversion", +#endif "-x", "c++" // Treat .h as C++, not C }; @@ -287,7 +292,12 @@ static void setupTarget(CXTranslationUnit translationUnit) { QTextStream str(&message); str << "CLANG v" << CINDEX_VERSION_MAJOR << '.' << CINDEX_VERSION_MINOR - << " targeting \"" << targetTriple() << "\", " << pointerSize() << "bit."; + << " targeting \"" << targetTriple() << "\"/" + << clang::compilerTripletValue(clang::compiler()) + << ", " << pointerSize() << "bit"; + if (clang::isCrossCompilation()) + str << ", (cross build)"; + str << '.'; } qCInfo(lcShiboken, "%s", qPrintable(message)); ReportHandler::addGeneralMessage(message + u'\n'); diff --git a/sources/shiboken6/ApiExtractor/clangparser/compilersupport.cpp b/sources/shiboken6/ApiExtractor/clangparser/compilersupport.cpp index 4d93a084..23e1f0ae 100644 --- a/sources/shiboken6/ApiExtractor/clangparser/compilersupport.cpp +++ b/sources/shiboken6/ApiExtractor/clangparser/compilersupport.cpp @@ -13,51 +13,82 @@ #include #include #include +#include +#include #include #include #include -#include #include #include +#include #include +#include using namespace Qt::StringLiterals; namespace clang { +// The command line options set +enum OptionSetFlag : unsigned +{ + CompilerOption = 0x1, + CompilerPathOption = 0x2, + PlatformOption = 0x4, + PlatformVersionOption = 0x8, + ArchitectureOption = 0x10 +}; + +Q_DECLARE_FLAGS(OptionsSet, OptionSetFlag) +Q_DECLARE_OPERATORS_FOR_FLAGS(OptionsSet) + +static OptionsSet setOptions; + QVersionNumber libClangVersion() { return QVersionNumber(CINDEX_VERSION_MAJOR, CINDEX_VERSION_MINOR); } -static Compiler _compiler = +static Compiler hostCompiler() +{ #if defined (Q_CC_CLANG) - Compiler::Clang; + return Compiler::Clang; #elif defined (Q_CC_MSVC) - Compiler::Msvc; + return Compiler::Msvc; #else - Compiler::Gpp; + return Compiler::Gpp; #endif +} + +static Compiler _compiler = hostCompiler(); Compiler compiler() { return _compiler; } -bool setCompiler(const QString &name) +// CMAKE_CXX_COMPILER_ID or triplet name +bool parseCompiler(QStringView name, Compiler *c) { bool result = true; - if (name == u"msvc") - _compiler = Compiler::Msvc; - else if (name == u"g++") - _compiler = Compiler::Gpp; - else if (name == u"clang") - _compiler = Compiler::Clang; + *c = hostCompiler(); + if (name.compare("msvc"_L1, Qt::CaseInsensitive) == 0) + *c = Compiler::Msvc; + else if (name.compare("g++"_L1, Qt::CaseInsensitive) == 0 || name.compare("gnu"_L1, Qt::CaseInsensitive) == 0) + *c = Compiler::Gpp; + else if (name.compare("clang"_L1, Qt::CaseInsensitive) == 0) + *c = Compiler::Clang; else result = false; return result; } +bool setCompiler(const QString &name) +{ + setOptions.setFlag(CompilerOption); + return parseCompiler(name, &_compiler); +} + QString _compilerPath; // Pre-defined compiler path (from command line) +QStringList _compilerArguments; // Arguments static unsigned _pointerSize = QT_POINTER_SIZE * 8; static QString _targetTriple; @@ -69,31 +100,249 @@ const QString &compilerPath() void setCompilerPath(const QString &name) { + setOptions.setFlag(CompilerPathOption); _compilerPath = name; } -static Platform _platform = +void addCompilerArgument(const QString &arg) +{ + _compilerArguments.append(arg); +} + +static Platform hostPlatform() +{ #if defined (Q_OS_DARWIN) - Platform::macOS; + return Platform::macOS; #elif defined (Q_OS_WIN) - Platform::Windows; + return Platform::Windows; +#elif defined (Q_OS_LINUX) + return Platform::Linux; #else - Platform::Unix; + return Platform::Unix; #endif +} + +static Platform _platform = hostPlatform(); Platform platform() { return _platform; } -bool setPlatform(const QString &name) +// from CMAKE_SYSTEM_NAME / legacy lower case name or target triplet +static bool parsePlatform(QStringView name, Platform *p) { + *p = hostPlatform(); bool result = true; - if (name == u"windows") - _platform = Platform::Windows; - else if (name == u"darwin") - _platform = Platform::macOS; - else if (name == u"unix") - _platform = Platform::Unix; - else + if (name.compare("unix"_L1, Qt::CaseInsensitive) == 0) { + *p = Platform::Unix; + } else if (name.compare("linux"_L1, Qt::CaseInsensitive) == 0) { + *p = Platform::Linux; + } else if (name.compare("windows"_L1, Qt::CaseInsensitive) == 0) { + *p = Platform::Windows; + } else if (name.compare("darwin"_L1, Qt::CaseInsensitive) == 0 + || name.compare("macosx"_L1, Qt::CaseInsensitive) == 0) { + *p = Platform::macOS; + } else if (name.startsWith("android"_L1, Qt::CaseInsensitive)) { + *p = Platform::Android; // "androideabi" + } else if (name.compare("ios"_L1, Qt::CaseInsensitive) == 0) { + *p = Platform::iOS; + } else { result = false; + } + return result; +} + +bool setPlatform(const QString &name) +{ + setOptions.setFlag(PlatformOption); + return parsePlatform(name, &_platform); +} + +static QVersionNumber hostPlatformVersion() +{ + auto ov = QOperatingSystemVersion::current(); + return ov.type() != QOperatingSystemVersionBase::Unknown ? ov.version() : QVersionNumber{}; +} + +// Version is not initialized from host since it is optional and the host version +// should not interfere with cross build targets +static QVersionNumber _platformVersion; + +QVersionNumber platformVersion() +{ + return _platformVersion; +} + +bool setPlatformVersion(const QString &name) +{ + auto v = QVersionNumber::fromString(name); + setOptions.setFlag(PlatformVersionOption); + const bool result = !v.isNull(); + if (result) + _platformVersion = v; + return result; +} + +static Architecture hostArchitecture() +{ + // src/corelib/global/archdetect.cpp, "Qt 6.9.2 (x86_64-little_endian-lp64..." + std::string_view build = QLibraryInfo::build(); + auto startPos = build.find('('); + auto dashPos = build.find('-'); + if (startPos != std::string_view::npos && dashPos != std::string_view::npos) { + ++startPos; + build = build.substr(startPos, dashPos - startPos); + if (build == "x86_64") + return Architecture::X64; + if (build == "i386") + return Architecture::X86; + if (build == "arm64") + return Architecture::Arm64; + if (build == "arm") + return Architecture::Arm32; + } + return Architecture::Other; +} + +// from CMAKE_SYSTEM_PROCESSOR or target triplet +static Architecture parseArchitecture(QStringView a) +{ + if (a == "AMD64"_L1 || a == "IA64"_L1 // Windows + || a == "x86_64"_L1) + return Architecture::X64; + if (a.compare("x86"_L1, Qt::CaseInsensitive) == 0 + || a.compare("i386"_L1, Qt::CaseInsensitive) == 0 + || a.compare("i486"_L1, Qt::CaseInsensitive) == 0 + || a.compare("i586"_L1, Qt::CaseInsensitive) == 0 + || a.compare("i686"_L1, Qt::CaseInsensitive) == 0) { + return Architecture::X86; + } + if (a.startsWith("armv7"_L1, Qt::CaseInsensitive)) + return Architecture::Arm32; + if (a.startsWith("arm"_L1, Qt::CaseInsensitive) + || a.startsWith("aarch64"_L1, Qt::CaseInsensitive)) { + return Architecture::Arm64; + } + return Architecture::Other; +} + +static Architecture _architecture = hostArchitecture(); + +Architecture architecture() +{ + return _architecture; +} + +bool setArchitecture(const QString &name) +{ + setOptions.setFlag(ArchitectureOption); + auto newArchitecture = parseArchitecture(name); + const bool result = newArchitecture != Architecture::Other; + if (result) + _architecture = newArchitecture; + return result; +} + +// Parsing triplets +static inline bool isVersionChar(QChar c) +{ + return c.isDigit() || c == u'.'; +} + +// "macosx15.0" -> "macosx" +QStringView stripTrailingVersion(QStringView s) +{ + while (!s.isEmpty() && isVersionChar(s.at(s.size() - 1))) + s.chop(1); + return s; +} + +bool parseTriplet(QStringView name, Architecture *a, Platform *p, Compiler *c, + QVersionNumber *version) +{ + *a = hostArchitecture(); + *p = hostPlatform(); + *c = hostCompiler(); + *version = hostPlatformVersion(); + auto values = name.split(u'-'); + if (values.size() < 2) + return false; + *a = parseArchitecture(values.constFirst()); + if (*a == Architecture::Other) + return false; + // Try a trailing compiler? + Compiler comp{}; + if (parseCompiler(stripTrailingVersion(values.constLast()), &comp)) { + *c = comp; + values.removeLast(); + } + const QStringView &fullPlatform = values.constLast(); + QStringView platformName = stripTrailingVersion(fullPlatform); + if (platformName.size() < fullPlatform.size()) { + if (auto vn = QVersionNumber::fromString(fullPlatform.sliced(platformName.size())); !vn.isNull()) + *version = vn; + } + return parsePlatform(platformName, p); +} + +const char *compilerTripletValue(Compiler c) +{ + switch (c) { + case Compiler::Clang: + return "clang"; + case Compiler::Msvc: + return "msvc"; + case Compiler::Gpp: + break; + } + return "gnu"; +} + +QByteArray targetTripletForPlatform(Platform p, Architecture a, Compiler c, + const QVersionNumber &platformVersion) +{ + QByteArray result; + if (p == Platform::Unix || a == Architecture::Other) + return result; // too unspecific + + switch (a) { + case Architecture::Other: + break; + case Architecture::X64: + result += "x86_64"; + break; + case Architecture::X86: + result += "i586"; + break; + case Architecture::Arm32: + result += "armv7a"; + break; + case Architecture::Arm64: + result += p == Platform::Android ? "aarch64" : "arm64"; + break; + } + + result += '-'; + + const QByteArray platformVersionB = platformVersion.isNull() + ? QByteArray{} : platformVersion.toString().toUtf8(); + switch (p) { + case Platform::Unix: + break; + case Platform::Linux: + result += "unknown-linux"_ba + platformVersionB + '-' + compilerTripletValue(c); + break; + case Platform::Windows: + result += "pc-windows"_ba + platformVersionB + '-' + compilerTripletValue(c); + break; + case Platform::macOS: + result += "apple-macosx"_ba + platformVersionB; + break; + case Platform::Android: + result += "unknown-linux-android"_ba + platformVersionB; + break; + case Platform::iOS: + result += "apple-ios"_ba + platformVersionB; + break; + } return result; } @@ -179,10 +428,12 @@ static void filterHomebrewHeaderPaths(HeaderPaths &headerPaths) // /usr/local/include // /System/Library/Frameworks (framework directory) // End of search list. -static HeaderPaths gppInternalIncludePaths(const QString &compiler) +static HeaderPaths gppInternalIncludePaths(const QString &compiler, + const QStringList &args) { HeaderPaths result; QStringList arguments{u"-E"_s, u"-x"_s, u"c++"_s, u"-"_s, u"-v"_s}; + arguments.append(args); QByteArray stdOut; QByteArray stdErr; if (!runProcess(compiler, arguments, &stdOut, &stdErr)) @@ -213,7 +464,8 @@ static HeaderPaths gppInternalIncludePaths(const QString &compiler) QString message; { QTextStream str(&message); - str << "gppInternalIncludePaths:\n compiler: " << compiler << '\n'; + str << "gppInternalIncludePaths:\n compiler: " << compiler + << arguments.join(u' ') << '\n'; for (const auto &h : result) str << " " << h.path << '\n'; if (ReportHandler::isDebug(ReportHandler::MediumDebug)) @@ -239,16 +491,10 @@ QByteArrayList detectVulkan() // For MSVC, we set the MS compatibility version and let Clang figure out its own // options and include paths. -// For the others, we pass "-nostdinc" since libclang tries to add it's own system -// include paths, which together with the clang compiler paths causes some clash -// which causes std types not being found and construct -I/-F options from the -// include paths of the host compiler. - -static QByteArray noStandardIncludeOption() { return QByteArrayLiteral("-nostdinc"); } // The clang builtin includes directory is used to find the definitions for // intrinsic functions and builtin types. It is necessary to use the clang -// includes to prevent redefinition errors. The default toolchain includes +// includes to prevent rqedefinition errors. The default toolchain includes // should be picked up automatically by clang without specifying // them implicitly. @@ -399,8 +645,8 @@ QByteArrayList emulatedCompilerOptions(LanguageLevel level) appendClangBuiltinIncludes(&headerPaths); break; case Compiler::Clang: - headerPaths.append(gppInternalIncludePaths(compilerFromCMake(u"clang++"_s))); - result.append(noStandardIncludeOption()); + headerPaths.append(gppInternalIncludePaths(compilerFromCMake(u"clang++"_s), + _compilerArguments)); break; case Compiler::Gpp: if (needsClangBuiltinIncludes()) @@ -408,7 +654,8 @@ QByteArrayList emulatedCompilerOptions(LanguageLevel level) // Append the c++ include paths since Clang is unable to find // etc (g++ 11.3). - const HeaderPaths gppPaths = gppInternalIncludePaths(compilerFromCMake(u"g++"_s)); + const HeaderPaths gppPaths = gppInternalIncludePaths(compilerFromCMake(u"g++"_s), + _compilerArguments); for (const HeaderPath &h : gppPaths) { if (h.path.contains("c++") || h.path.contains("sysroot")) headerPaths.append(h); @@ -416,6 +663,17 @@ QByteArrayList emulatedCompilerOptions(LanguageLevel level) break; } + // For Android cross-compilation, prevent clang from including host system headers that + // conflict with NDK headers. + // This addresses the mbstate_t typedef redefinition error in COIN for RHEL 9.4 + const auto hostPlatform = clang::hostPlatform(); + if (_platform == Platform::Android + && (hostPlatform == Platform::Unix || hostPlatform == Platform::Linux) + && (_compiler == Compiler::Clang || _compiler == Compiler::Gpp)) { + result.append("-nostdinc"); + result.append("-nostdinc++"); + } + std::transform(headerPaths.cbegin(), headerPaths.cend(), std::back_inserter(result), HeaderPath::includeOption); return result; @@ -453,7 +711,7 @@ const char *languageLevelOption(LanguageLevel l) LanguageLevel languageLevelFromOption(const char *o) { for (const LanguageLevelMapping &m : languageLevelMapping) { - if (!strcmp(m.option, o)) + if (!std::strcmp(m.option, o)) return m.level; } return LanguageLevel::Default; @@ -479,13 +737,65 @@ void setTargetTriple(const QString &t) _targetTriple = t; } -void setTargetTriple(const QStringList &clangOptions) +bool isCrossCompilation() { - static constexpr auto targetOption = "--target="_L1; - auto targetOptionPred = [](const QString &o) { return o.startsWith(targetOption); }; - const auto it = std::find_if(clangOptions.cbegin(), clangOptions.cend(), targetOptionPred); - if (it != clangOptions.cend()) - _targetTriple = it->sliced(targetOption.size()); + return platform() != hostPlatform() || architecture() != hostArchitecture() + || compiler() != hostCompiler(); +} + +static const char targetOptionC[] = "--target="; + +static inline bool isTargetOption(const QByteArray &o) +{ + return o.startsWith(targetOptionC); +} + +static bool isTargetArchOption(const QByteArray &o) +{ + return isTargetOption(o) + || o.startsWith("-march=") || o.startsWith("-meabi"); +} + +bool hasTargetOption(const QByteArrayList &clangOptions) +{ + return std::any_of(clangOptions.cbegin(), clangOptions.cend(), + isTargetArchOption); +} + +void setHeuristicOptions(const QByteArrayList &clangOptions) +{ + // Figure out compiler type from the binary set + if (!setOptions.testFlag(CompilerOption) && setOptions.testFlag(CompilerPathOption)) { + const QString name = QFileInfo(_compilerPath).baseName().toLower(); + if (name.contains("clang"_L1)) + _compiler = Compiler::Clang; + else if (name.contains("cl"_L1)) + _compiler = Compiler::Msvc; + else if (name.contains("gcc"_L1) || name.contains("g++"_L1)) + _compiler = Compiler::Gpp; + } + + // Figure out platform/arch from "--target" triplet + if (!setOptions.testFlag(PlatformOption) && !setOptions.testFlag(ArchitectureOption)) { + auto it = std::find_if(clangOptions.cbegin(), clangOptions.cend(), isTargetOption); + if (it != clangOptions.cend()) { + const QString triplet = QLatin1StringView(it->sliced(qstrlen(targetOptionC))); + Architecture arch{}; + Platform platform{}; + Compiler comp{}; + QVersionNumber platformVersion; + if (parseTriplet(triplet, &arch, &platform, &comp, &platformVersion)) { + if (!setOptions.testFlag(ArchitectureOption)) + _architecture = arch; + if (!setOptions.testFlag(PlatformOption)) + _platform = platform; + if (!setOptions.testFlag(PlatformVersionOption)) + _platformVersion = platformVersion; + } else { + qCWarning(lcShiboken, "Unable to parse triplet \"%s\".", qPrintable(triplet)); + } + } + } } } // namespace clang diff --git a/sources/shiboken6/ApiExtractor/clangparser/compilersupport.h b/sources/shiboken6/ApiExtractor/clangparser/compilersupport.h index 0e12ca13..84395d28 100644 --- a/sources/shiboken6/ApiExtractor/clangparser/compilersupport.h +++ b/sources/shiboken6/ApiExtractor/clangparser/compilersupport.h @@ -5,8 +5,8 @@ #define COMPILERSUPPORT_H #include +#include -QT_FORWARD_DECLARE_CLASS(QVersionNumber) QT_FORWARD_DECLARE_CLASS(QString) enum class LanguageLevel { @@ -26,8 +26,19 @@ enum class Compiler { enum class Platform { Unix, + Linux, Windows, - macOS + macOS, + Android, + iOS +}; + +enum class Architecture { + Other, + X64, + X86, + Arm64, + Arm32 }; namespace clang { @@ -48,17 +59,41 @@ QString compilerFromCMake(); const QString &compilerPath(); void setCompilerPath(const QString &name); +void addCompilerArgument(const QString &arg); Platform platform(); bool setPlatform(const QString &name); +QVersionNumber platformVersion(); +bool setPlatformVersion(const QString &name); + +QByteArray targetTripletForPlatform(Platform p, Architecture a, Compiler c, + const QVersionNumber &platformVersion = {}); +const char *compilerTripletValue(Compiler c); + +Architecture architecture(); +bool setArchitecture(const QString &name); + unsigned pointerSize(); // (bit) void setPointerSize(unsigned ps); // Set by parser QString targetTriple(); -void setTargetTriple(const QStringList &clangOptions); // Set from cmd line before parsing void setTargetTriple(const QString &t); // Updated by clang parser while parsing +bool isCrossCompilation(); + +// Are there any options specifying a target +bool hasTargetOption(const QByteArrayList &clangOptions); + +// Unless the platform/architecture/compiler options were set, try to find +// values based on a --target option in clangOptions and the compiler path. +void setHeuristicOptions(const QByteArrayList &clangOptions); + +// Parse a triplet "x86_64-unknown-linux-gnu" (for testing). Note the +// compiler might not be present and defaults to host +bool parseTriplet(QStringView name, Architecture *a, Platform *p, Compiler *c, + QVersionNumber *version); + } // namespace clang #endif // COMPILERSUPPORT_H diff --git a/sources/shiboken6/ApiExtractor/classdocumentation.cpp b/sources/shiboken6/ApiExtractor/classdocumentation.cpp index 1e721b58..ccc7b29d 100644 --- a/sources/shiboken6/ApiExtractor/classdocumentation.cpp +++ b/sources/shiboken6/ApiExtractor/classdocumentation.cpp @@ -88,7 +88,7 @@ enum class WebXmlCodeTag static WebXmlCodeTag tag(QStringView name) { - if (name == u"class" || name == u"namespace") + if (name == "class"_L1 || name == "struct"_L1 || name == "namespace"_L1) return WebXmlCodeTag::Class; if (name == u"enum") return WebXmlCodeTag::Enum; diff --git a/sources/shiboken6/ApiExtractor/conditionalstreamreader.cpp b/sources/shiboken6/ApiExtractor/conditionalstreamreader.cpp index 6b9f0ae2..88079a63 100644 --- a/sources/shiboken6/ApiExtractor/conditionalstreamreader.cpp +++ b/sources/shiboken6/ApiExtractor/conditionalstreamreader.cpp @@ -158,24 +158,7 @@ bool ConditionalStreamReader::conditionMatches() const void ConditionalStreamReader::setConditions(const QStringList &newConditions) { - m_conditions = newConditions + platformConditions(); -} - -QStringList ConditionalStreamReader::platformConditions() -{ - QStringList result; -#if defined (Q_OS_UNIX) - result << "unix"_L1; -#endif - -#if defined (Q_OS_LINUX) - result << "linux"_L1; -#elif defined (Q_OS_MACOS) - result << "darwin"_L1; -#elif defined (Q_OS_WINDOWS) - result << "windows"_L1; -#endif - return result; + m_conditions = newConditions; } ConditionalStreamReader::ExtendedToken ConditionalStreamReader::readNextInternal() diff --git a/sources/shiboken6/ApiExtractor/conditionalstreamreader.h b/sources/shiboken6/ApiExtractor/conditionalstreamreader.h index 36c4752a..d9af5dc1 100644 --- a/sources/shiboken6/ApiExtractor/conditionalstreamreader.h +++ b/sources/shiboken6/ApiExtractor/conditionalstreamreader.h @@ -69,8 +69,6 @@ public: const QStringList &conditions() const { return m_conditions; } void setConditions(const QStringList &newConditions); - static QStringList platformConditions(); - private: enum class PiTokens { None, If, Endif, EntityDefinition }; @@ -82,7 +80,7 @@ private: QXmlStreamReader m_reader; ProxyEntityResolver *m_proxyEntityResolver = nullptr; - QStringList m_conditions = ConditionalStreamReader::platformConditions(); + QStringList m_conditions; }; QDebug operator<<(QDebug dbg, const QXmlStreamAttributes &a); diff --git a/sources/shiboken6/ApiExtractor/customconversion.cpp b/sources/shiboken6/ApiExtractor/customconversion.cpp index 4cfd1b97..b022cd69 100644 --- a/sources/shiboken6/ApiExtractor/customconversion.cpp +++ b/sources/shiboken6/ApiExtractor/customconversion.cpp @@ -5,6 +5,7 @@ #include "containertypeentry.h" #include "customtypenentry.h" #include "primitivetypeentry.h" +#include "smartpointertypeentry.h" #include "valuetypeentry.h" #include @@ -94,6 +95,11 @@ QString TargetToNativeConversion::sourceTypeName() const } QString TargetToNativeConversion::sourceTypeCheck() const +{ + return m_sourceTypeCheck; +} + +QString TargetToNativeConversion::sourceTypeCheckFallback() const { if (!m_sourceTypeCheck.isEmpty()) return m_sourceTypeCheck; @@ -108,6 +114,10 @@ QString TargetToNativeConversion::sourceTypeCheck() const } } + if (m_sourceTypeName == "Py_None"_L1 || m_sourceTypeName == "PyNone"_L1) + return "%in == Py_None"_L1; + if (m_sourceTypeName == "SbkObject"_L1) + return "Shiboken::Object::checkType(%in)"_L1; return {}; } @@ -139,6 +149,8 @@ CustomConversionPtr CustomConversion::getCustomConversion(const TypeEntryCPtr &t return std::static_pointer_cast(type)->customConversion(); if (type->isValue()) return std::static_pointer_cast(type)->customConversion(); + if (type->isSmartPointer()) + return std::static_pointer_cast(type)->customConversion(); return {}; } diff --git a/sources/shiboken6/ApiExtractor/customconversion.h b/sources/shiboken6/ApiExtractor/customconversion.h index a7517884..9125d670 100644 --- a/sources/shiboken6/ApiExtractor/customconversion.h +++ b/sources/shiboken6/ApiExtractor/customconversion.h @@ -25,7 +25,10 @@ public: void setSourceType(const TypeEntryCPtr &sourceType); bool isCustomType() const; QString sourceTypeName() const; + // Check as specified in the type system QString sourceTypeCheck() const; + // Check with fallback bases on sourceType + QString sourceTypeCheckFallback() const; QString conversion() const; void setConversion(const QString &conversion); diff --git a/sources/shiboken6/ApiExtractor/documentation.h b/sources/shiboken6/ApiExtractor/documentation.h index 580d8f96..a623529c 100644 --- a/sources/shiboken6/ApiExtractor/documentation.h +++ b/sources/shiboken6/ApiExtractor/documentation.h @@ -29,6 +29,7 @@ public: bool equals(const Documentation &rhs) const; + bool hasDetailed() const { return !m_detailed.isEmpty(); } const QString &detailed() const { return m_detailed; } void setDetailed(const QString &detailed); diff --git a/sources/shiboken6/ApiExtractor/enumtypeentry.h b/sources/shiboken6/ApiExtractor/enumtypeentry.h index 3360d7db..633ab318 100644 --- a/sources/shiboken6/ApiExtractor/enumtypeentry.h +++ b/sources/shiboken6/ApiExtractor/enumtypeentry.h @@ -13,6 +13,12 @@ class EnumTypeEntryPrivate; class EnumTypeEntry : public ConfigurableTypeEntry { public: + enum AliasMode : unsigned char { + NoAlias, + AliasSource, // Source of a C++ alias "using ThatEnum = ThisEnum"; + AliasTarget // Target of a C++ alias "using ThisEnum = ThatEnum"; + }; + explicit EnumTypeEntry(const QString &entryName, const QVersionNumber &vr, const TypeEntryCPtr &parent); @@ -40,6 +46,12 @@ public: QString docFile() const; void setDocFile(const QString &df); + AliasMode aliasMode() const; + void setAliasMode(AliasMode am); + + EnumTypeEntryCPtr aliasTypeEntry() const; + void setAliasTypeEntry(const EnumTypeEntryCPtr &entry); + TypeEntry *clone() const override; #ifndef QT_NO_DEBUG_STREAM void formatDebug(QDebug &d) const override; diff --git a/sources/shiboken6/ApiExtractor/fileout.cpp b/sources/shiboken6/ApiExtractor/fileout.cpp index 2aa7a549..dbbfc349 100644 --- a/sources/shiboken6/ApiExtractor/fileout.cpp +++ b/sources/shiboken6/ApiExtractor/fileout.cpp @@ -12,6 +12,8 @@ #include +using namespace Qt::StringLiterals; + bool FileOut::m_dryRun = false; bool FileOut::m_diff = false; @@ -175,8 +177,8 @@ FileOut::State FileOut::done() if (!FileOut::m_dryRun) { QDir dir(info.absolutePath()); if (!dir.mkpath(dir.absolutePath())) { - const QString message = QString::fromLatin1("Unable to create directory '%1'") - .arg(QDir::toNativeSeparators(dir.absolutePath())); + const QString message = "Unable to create directory '%1'"_L1 + .arg(QDir::toNativeSeparators(dir.absolutePath())); throw Exception(message); } diff --git a/sources/shiboken6/ApiExtractor/messages.cpp b/sources/shiboken6/ApiExtractor/messages.cpp index 5647cccc..6932b04d 100644 --- a/sources/shiboken6/ApiExtractor/messages.cpp +++ b/sources/shiboken6/ApiExtractor/messages.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -171,7 +172,7 @@ static void msgFormatEnumType(Stream &str, { switch (enumItem->enumKind()) { case CEnum: - str << "Enum '" << enumItem->qualifiedName().join(u"::"_s) << '\''; + str << "Enum '" << enumItem->qualifiedNameString() << '\''; break; case AnonymousEnum: { const EnumeratorList &values = enumItem->enumerators(); @@ -194,7 +195,7 @@ static void msgFormatEnumType(Stream &str, } break; case EnumClass: - str << "Scoped enum '" << enumItem->qualifiedName().join(u"::"_s) << '\''; + str << "Scoped enum '" << enumItem->qualifiedNameString() << '\''; break; } if (!className.isEmpty()) @@ -715,13 +716,13 @@ QString msgXpathDocModificationError(const DocModificationList& mods, QString msgCannotOpenForReading(const QFile &f) { - return QString::fromLatin1("Failed to open file '%1' for reading: %2") + return "Failed to open file '%1' for reading: %2"_L1 .arg(QDir::toNativeSeparators(f.fileName()), f.errorString()); } QString msgCannotOpenForWriting(const QFile &f) { - return QString::fromLatin1("Failed to open file '%1' for writing: %2") + return "Failed to open file '%1' for writing: %2"_L1 .arg(QDir::toNativeSeparators(f.fileName()), f.errorString()); } @@ -1021,7 +1022,7 @@ QString msgUnknownArrayPointerConversion(const QString &s) QString msgMissingProjectFileMarker(const QString &name, const QByteArray &startMarker) { return u"First line of project file \""_s + QDir::toNativeSeparators(name) - + u"\" must be the string \""_s + QString::fromLatin1(startMarker) + u"\"."_s; + + u"\" must be the string \""_s + QLatin1StringView(startMarker) + u"\"."_s; } QString msgInvalidLanguageLevel(const QString &l) @@ -1101,6 +1102,7 @@ QString msgRemoveRedundantOverload(const AbstractMetaFunctionCPtr &func, QString msgCommandLineArguments(const QStringList &argv) { QString result = "Host platform: "_L1 + QLatin1StringView(QLibraryInfo::build()) + + "\nHost OS : "_L1 + QSysInfo::prettyProductName() + "\nCommand line:\n "_L1; for (const QString &arg : argv) { result.append(u' '); diff --git a/sources/shiboken6/ApiExtractor/parser/codemodel.cpp b/sources/shiboken6/ApiExtractor/parser/codemodel.cpp index a5994bd7..8a56c9ee 100644 --- a/sources/shiboken6/ApiExtractor/parser/codemodel.cpp +++ b/sources/shiboken6/ApiExtractor/parser/codemodel.cpp @@ -160,6 +160,11 @@ QStringList _CodeModelItem::qualifiedName() const return q; } +QString _CodeModelItem::qualifiedNameString() const +{ + return qualifiedName().join("::"_L1); +} + QString _CodeModelItem::name() const { return m_name; @@ -453,11 +458,6 @@ FunctionModelItem _ScopeModelItem::declaredFunction(const FunctionModelItem &ite _ScopeModelItem::~_ScopeModelItem() = default; -void _ScopeModelItem::addEnumsDeclaration(const QString &enumsDeclaration) -{ - m_enumsDeclarations << enumsDeclaration; -} - void _ScopeModelItem::addClass(const ClassModelItem &item) { m_classes.append(item); @@ -520,15 +520,13 @@ void _ScopeModelItem::appendScope(const _ScopeModelItem &other) m_templateTypeAliases += other.m_templateTypeAliases; m_variables += other.m_variables; m_functions += other.m_functions; - m_enumsDeclarations += other.m_enumsDeclarations; } bool _ScopeModelItem::isEmpty() const { return m_classes.isEmpty() && m_enums.isEmpty() && m_typeDefs.isEmpty() && m_templateTypeAliases.isEmpty() - && m_variables.isEmpty() && m_functions.isEmpty() - && m_enumsDeclarations.isEmpty(); + && m_variables.isEmpty() && m_functions.isEmpty(); } /* This function removes MSVC export declarations of non-type template @@ -1208,11 +1206,26 @@ void _TypeDefModelItem::setType(const TypeInfo &type) m_type = type; } +TypeCategory _TypeDefModelItem::underlyingTypeCategory() const +{ + return m_type.typeCategory(); +} + +Access _TypeDefModelItem::accessPolicy() const +{ + return m_accessPolicy; +} + +void _TypeDefModelItem::setAccessPolicy(Access accessPolicy) +{ + m_accessPolicy = accessPolicy; +} + #ifndef QT_NO_DEBUG_STREAM void _TypeDefModelItem::formatDebug(QDebug &d) const { _CodeModelItem::formatDebug(d); - d << ", type=" << m_type; + d << ", " << m_accessPolicy << ", type=" << m_type; } #endif // !QT_NO_DEBUG_STREAM diff --git a/sources/shiboken6/ApiExtractor/parser/codemodel.h b/sources/shiboken6/ApiExtractor/parser/codemodel.h index fb46fab8..77082efa 100644 --- a/sources/shiboken6/ApiExtractor/parser/codemodel.h +++ b/sources/shiboken6/ApiExtractor/parser/codemodel.h @@ -120,6 +120,7 @@ public: int kind() const; QStringList qualifiedName() const; + QString qualifiedNameString() const; QString name() const; void setName(const QString &name); @@ -211,9 +212,6 @@ public: TemplateTypeAliasModelItem findTemplateTypeAlias(QAnyStringView name) const; VariableModelItem findVariable(QAnyStringView name) const; - void addEnumsDeclaration(const QString &enumsDeclaration); - QStringList enumsDeclarations() const { return m_enumsDeclarations; } - FunctionModelItem declaredFunction(const FunctionModelItem &item); bool isEmpty() const; @@ -250,9 +248,6 @@ private: TemplateTypeAliasList m_templateTypeAliases; VariableList m_variables; FunctionList m_functions; - -private: - QStringList m_enumsDeclarations; }; class _ClassModelItem: public _ScopeModelItem @@ -564,11 +559,17 @@ public: TypeInfo type() const; void setType(const TypeInfo &type); + TypeCategory underlyingTypeCategory() const; + + Access accessPolicy() const; + void setAccessPolicy(Access accessPolicy); + #ifndef QT_NO_DEBUG_STREAM void formatDebug(QDebug &d) const override; #endif private: + Access m_accessPolicy = Access::Public; TypeInfo m_type; }; diff --git a/sources/shiboken6/ApiExtractor/parser/codemodel_enums.h b/sources/shiboken6/ApiExtractor/parser/codemodel_enums.h index e5c429bd..c3bb10e5 100644 --- a/sources/shiboken6/ApiExtractor/parser/codemodel_enums.h +++ b/sources/shiboken6/ApiExtractor/parser/codemodel_enums.h @@ -58,4 +58,14 @@ enum class FunctionAttribute { Q_DECLARE_FLAGS(FunctionAttributes, FunctionAttribute) Q_DECLARE_OPERATORS_FOR_FLAGS(FunctionAttributes) +// C++ type category for TypeInfo, reflecting clang's CXTypeKind +enum class TypeCategory : unsigned char { + Other, + Builtin, + Enum, + Pointer, + Function, + Void +}; + #endif // CODEMODEL_ENUMS_H diff --git a/sources/shiboken6/ApiExtractor/parser/typeinfo.cpp b/sources/shiboken6/ApiExtractor/parser/typeinfo.cpp index 93627e6d..c530cafe 100644 --- a/sources/shiboken6/ApiExtractor/parser/typeinfo.cpp +++ b/sources/shiboken6/ApiExtractor/parser/typeinfo.cpp @@ -25,7 +25,6 @@ class TypeInfoData : public QSharedData public: TypeInfoData(); - bool isVoid() const; bool equals(const TypeInfoData &other) const; bool isStdType() const; void simplifyStdType(); @@ -48,6 +47,7 @@ public: }; ReferenceType m_referenceType = NoReference; + TypeCategory m_category = TypeCategory::Other; }; TypeInfoData::TypeInfoData() : flags(0) @@ -143,18 +143,21 @@ void TypeInfo::addName(const QString &n) d->m_qualifiedName.append(n); } -bool TypeInfoData::isVoid() const +bool TypeInfo::isVoid() const { - return m_indirections.isEmpty() && m_referenceType == NoReference - && m_arguments.isEmpty() && m_arrayElements.isEmpty() - && m_instantiations.isEmpty() - && m_qualifiedName.size() == 1 - && m_qualifiedName.constFirst() == u"void"; + return d->m_category == TypeCategory::Void; } -bool TypeInfo::isVoid() const +TypeCategory TypeInfo::typeCategory() const +{ + return d->m_category; + +} + +void TypeInfo::setTypeCategory(TypeCategory c) { - return d->isVoid(); + if (d->m_category != c) + d->m_category = c; } bool TypeInfo::isConstant() const @@ -457,6 +460,7 @@ bool TypeInfoData::equals(const TypeInfoData &other) const return flags == other.flags && m_qualifiedName == other.m_qualifiedName + && m_category == other.m_category && (!m_functionPointer || m_arguments == other.m_arguments) && m_instantiations == other.m_instantiations; } @@ -584,6 +588,23 @@ void TypeInfo::formatDebug(QDebug &debug) const debug << ", [const]"; if (d->m_volatile) debug << ", [volatile]"; + switch (d->m_category) { + case TypeCategory::Other: + case TypeCategory::Void: + break; + case TypeCategory::Builtin: + debug << ", [builtin]"; + break; + case TypeCategory::Enum: + debug << ", [enum]"; + break; + case TypeCategory::Pointer: + debug << ", [pointer]"; + break; + case TypeCategory::Function: + debug << ", [function"; + break; + } if (!d->m_indirections.isEmpty()) { debug << ", indirections="; for (auto i : d->m_indirections) diff --git a/sources/shiboken6/ApiExtractor/parser/typeinfo.h b/sources/shiboken6/ApiExtractor/parser/typeinfo.h index 6f75b573..092fbb72 100644 --- a/sources/shiboken6/ApiExtractor/parser/typeinfo.h +++ b/sources/shiboken6/ApiExtractor/parser/typeinfo.h @@ -48,6 +48,9 @@ public: bool isVoid() const; + TypeCategory typeCategory() const; + void setTypeCategory(TypeCategory c); + bool isConstant() const; void setConstant(bool is); diff --git a/sources/shiboken6/ApiExtractor/predefined_templates.cpp b/sources/shiboken6/ApiExtractor/predefined_templates.cpp index 22727282..63994772 100644 --- a/sources/shiboken6/ApiExtractor/predefined_templates.cpp +++ b/sources/shiboken6/ApiExtractor/predefined_templates.cpp @@ -194,7 +194,7 @@ return %out; uR"(PyObject *%out = PyList_New(Py_ssize_t(%in.size())); Py_ssize_t idx = 0; for (auto it = std::cbegin(%in), end = std::cend(%in); it != end; ++it, ++idx) { - const auto &cppItem = *it; + ShibokenContainerStdVectorValueType<%INTYPE_0>::Type cppItem = *it; PyList_SetItem(%out, idx, %CONVERTTOPYTHON[%INTYPE_0](cppItem)); } return %out;)"_s}, diff --git a/sources/shiboken6/ApiExtractor/qtdocparser.cpp b/sources/shiboken6/ApiExtractor/qtdocparser.cpp index c7361b87..0d982b26 100644 --- a/sources/shiboken6/ApiExtractor/qtdocparser.cpp +++ b/sources/shiboken6/ApiExtractor/qtdocparser.cpp @@ -30,6 +30,8 @@ #include #include +#include +#include using namespace Qt::StringLiterals; @@ -245,23 +247,35 @@ QtDocParser::FunctionDocumentationOpt return std::nullopt; } -// Extract the section from a WebXML (class) documentation and remove it -// from the source. -static QString extractBrief(QString *value) +// Extract the /detailed sections from a WebXML (class) documentation (from ) +static std::pair extractBrief(QString value) { - const auto briefStart = value->indexOf(briefStartElement); - if (briefStart < 0) - return {}; - const auto briefEnd = value->indexOf(briefEndElement, - briefStart + briefStartElement.size()); - if (briefEnd < briefStart) - return {}; - const auto briefLength = briefEnd + briefEndElement.size() - briefStart; - QString briefValue = value->mid(briefStart, briefLength); - briefValue.insert(briefValue.size() - briefEndElement.size(), - u" More_..."_s); - value->remove(briefStart, briefLength); - return briefValue; + std::pair result; + const auto briefStart = value.indexOf(briefStartElement); + if (briefStart > 0) { + const auto briefEnd = value.indexOf(briefEndElement, + briefStart + briefStartElement.size()); + if (briefEnd > briefStart) { + const auto briefLength = briefEnd + briefEndElement.size() - briefStart; + if (briefLength > briefStartElement.size() + briefEndElement.size()) + result.first = value.sliced(briefStart, briefLength); + value.remove(briefStart, briefLength); + // Remove any space/newlines between the element and its + // surrounding XML elements. + auto lastElement = value.lastIndexOf(u'>', briefStart); + if (lastElement != -1) { + ++lastElement; + const auto nextElement = value.indexOf(u'<', briefStart); + if (nextElement > lastElement) + value.remove(lastElement, nextElement - lastElement); + } + } + } + + if (value != ""_L1) + result.second = value; + + return result; } // Apply the documentation parsed from WebXML to a AbstractMetaFunction and complete argument @@ -408,13 +422,12 @@ QString QtDocParser::fillDocumentation(const AbstractMetaClassPtr &metaClass) qCWarning(lcShibokenDoc, "%s", qPrintable(msgCannotFindDocumentation(sourceFileName, "class", className, {}))); } - const QString brief = extractBrief(&docString); + const auto descriptionPair = extractBrief(docString); Documentation doc; doc.setSourceFile(sourceFileName); - if (!brief.isEmpty()) - doc.setValue(brief, DocumentationType::Brief); - doc.setValue(docString); + doc.setValue(descriptionPair.first, DocumentationType::Brief); + doc.setValue(descriptionPair.second, DocumentationType::Detailed); metaClass->setDocumentation(doc); //Functions Documentation @@ -484,6 +497,23 @@ static QString qmlReferenceLink(const QFileInfo &qmlModuleFi) + u'/' + qmlModuleFi.baseName() + ".html"_L1; } +// Find a webxml file containing QML types. Note: These files are empty; +// we need to point to the web docs. +static std::optional qmlModuleFile(const QString &dirPath, + const QString &lowerModuleName) +{ + static constexpr auto postFix = "-qmlmodule.webxml"_L1; + const QFileInfo moduleFile(dirPath + u'/' + lowerModuleName + postFix); + if (moduleFile.exists()) + return moduleFile; + // Some file names are irregular, fall back to using a filter + const QFileInfoList qmlModuleFiles = + QDir(dirPath).entryInfoList({u'*' + postFix}, QDir::Files); + if (!qmlModuleFiles.isEmpty()) + return qmlModuleFiles.constFirst(); + return std::nullopt; +} + ModuleDocumentation QtDocParser::retrieveModuleDocumentation(const QString &name) { // TODO: This method of acquiring the module name supposes that the target language uses @@ -518,11 +548,8 @@ ModuleDocumentation QtDocParser::retrieveModuleDocumentation(const QString &name ModuleDocumentation result{Documentation{docString, {}, sourceFile}, {}}; // If a QML module info file exists, insert a link to the Qt docs. - // Use a filter as some file names are irregular. // Note: These files are empty; we need to point to the web docs. - const QFileInfoList qmlModuleFiles = - QDir(dirPath).entryInfoList({"*-qmlmodule.webxml"_L1}, QDir::Files); - if (!qmlModuleFiles.isEmpty()) - result.qmlTypesUrl = qmlReferenceLink(qmlModuleFiles.constFirst()); + if (const auto qmlModuleFileO = qmlModuleFile(dirPath, lowerModuleName)) + result.qmlTypesUrl = qmlReferenceLink(qmlModuleFileO.value()); return result; } diff --git a/sources/shiboken6/ApiExtractor/reporthandler.cpp b/sources/shiboken6/ApiExtractor/reporthandler.cpp index 95ea2ce4..e24dadb9 100644 --- a/sources/shiboken6/ApiExtractor/reporthandler.cpp +++ b/sources/shiboken6/ApiExtractor/reporthandler.cpp @@ -206,11 +206,13 @@ void ReportHandler::addGeneralMessage(const QString &message) generalMessages.append(message); } +static const char generalLogFile[] = "mjb_shiboken.log"; + void ReportHandler::writeGeneralLogFile(const QString &directory) { if (generalMessages.isEmpty()) return; - QFile file(directory + "/mjb_shiboken.log"_L1); + QFile file(directory + u'/' + QLatin1StringView(generalLogFile)); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qWarning(lcShiboken, "%s", qPrintable(msgCannotOpenForWriting(file))); return; @@ -220,3 +222,13 @@ void ReportHandler::writeGeneralLogFile(const QString &directory) file.putChar('\n'); } } + +void ReportHandler::dumpGeneralLogFile() +{ + std::fprintf(stdout, "\n--- %s ---\n", generalLogFile); + for (const auto &m : std::as_const(generalMessages)) { + std::fputs(m.toUtf8().constData(), stdout); + std::fputc('\n', stdout); + } + std::fprintf(stdout, "--- End of %s ---\n\n", generalLogFile); +} diff --git a/sources/shiboken6/ApiExtractor/reporthandler.h b/sources/shiboken6/ApiExtractor/reporthandler.h index 94449019..3562ab43 100644 --- a/sources/shiboken6/ApiExtractor/reporthandler.h +++ b/sources/shiboken6/ApiExtractor/reporthandler.h @@ -41,6 +41,7 @@ public: static void addGeneralMessage(const QString &message); static void writeGeneralLogFile(const QString &directory); + static void dumpGeneralLogFile(); private: static void messageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg); diff --git a/sources/shiboken6/ApiExtractor/smartpointertypeentry.h b/sources/shiboken6/ApiExtractor/smartpointertypeentry.h index d704210f..7b67647b 100644 --- a/sources/shiboken6/ApiExtractor/smartpointertypeentry.h +++ b/sources/shiboken6/ApiExtractor/smartpointertypeentry.h @@ -5,6 +5,7 @@ #define SMARTPOINTERTYPEENTRY_H #include "complextypeentry.h" +#include "customconversion_typedefs.h" class SmartPointerTypeEntryPrivate; @@ -51,6 +52,10 @@ public: QString getTargetName(const AbstractMetaType &metaType) const; + bool hasCustomConversion() const; + void setCustomConversion(const CustomConversionPtr &customConversion); + CustomConversionPtr customConversion() const; + #ifndef QT_NO_DEBUG_STREAM void formatDebug(QDebug &d) const override; #endif diff --git a/sources/shiboken6/ApiExtractor/tests/a.xml b/sources/shiboken6/ApiExtractor/tests/a.xml index 3c09d380..bb771ed1 100644 --- a/sources/shiboken6/ApiExtractor/tests/a.xml +++ b/sources/shiboken6/ApiExtractor/tests/a.xml @@ -3,7 +3,7 @@ - oi + before brief Brief description Paragraph number 1 Paragraph number 2 diff --git a/sources/shiboken6/ApiExtractor/tests/testextrainclude.cpp b/sources/shiboken6/ApiExtractor/tests/testextrainclude.cpp index a95b7150..989b05cd 100644 --- a/sources/shiboken6/ApiExtractor/tests/testextrainclude.cpp +++ b/sources/shiboken6/ApiExtractor/tests/testextrainclude.cpp @@ -7,9 +7,13 @@ #include #include #include +#include +#include #include +using namespace Qt::StringLiterals; + void TestExtraInclude::testClassExtraInclude() { const char cppCode[] = "struct A {};\n"; @@ -61,4 +65,97 @@ void TestExtraInclude::testGlobalExtraIncludes() QCOMPARE(includes.constLast().name(), u"header2.h"); } +void TestExtraInclude::testParseTriplet_data() +{ + QTest::addColumn("triplet"); + QTest::addColumn("expectedOk"); + QTest::addColumn("expectedArchitecture"); + QTest::addColumn("expectedPlatform"); + QTest::addColumn("expectedCompilerPresent"); + QTest::addColumn("expectedCompiler"); + QTest::addColumn("expectedPlatformVersionPresent"); + QTest::addColumn("expectedPlatformVersion"); + QTest::addColumn("expectedConverted"); // test back-conversion + + QTest::newRow("Invalid") + << QString("Invalid"_L1) + << false << Architecture::X64 << Platform::Linux << false << Compiler::Gpp + << false << QVersionNumber{} << QByteArray{}; + + QTest::newRow("Linux") + << QString("x86_64-unknown-linux-gnu"_L1) + << true << Architecture::X64 << Platform::Linux << true << Compiler::Gpp + << false << QVersionNumber{} + << "x86_64-unknown-linux-gnu"_ba; + + QTest::newRow("WindowsArm") + << QString("aarch64-pc-windows-msvc19.39.0"_L1) + << true << Architecture::Arm64 << Platform::Windows << true << Compiler::Msvc + << false << QVersionNumber{} + << "arm64-pc-windows-msvc"_ba; + + QTest::newRow("Apple") + << QString("arm64-apple-macosx15.0.0"_L1) + << true << Architecture::Arm64 << Platform::macOS << false << Compiler::Gpp + << true << QVersionNumber{15, 0, 0} + << "arm64-apple-macosx15.0.0"_ba; + + QTest::newRow("AndroidArm32") + << QString("armv7a-none-linux-android5.1"_L1) + << true << Architecture::Arm32 << Platform::Android << false << Compiler::Gpp + << true << QVersionNumber{5, 1} + << "armv7a-unknown-linux-android5.1"_ba; + + QTest::newRow("AndroidArm64") + << QString("aarch64-none-linux-androideabi27.1"_L1) + << true << Architecture::Arm64 << Platform::Android << false << Compiler::Gpp + << true << QVersionNumber{27, 1} + << "aarch64-unknown-linux-android27.1"_ba; + + QTest::newRow("iOS") + << QString("arm64-apple-ios"_L1) + << true << Architecture::Arm64 << Platform::iOS << false << Compiler::Gpp + << false << QVersionNumber{} + << "arm64-apple-ios"_ba; +} + +void TestExtraInclude::testParseTriplet() +{ + QFETCH(QString, triplet); + QFETCH(bool, expectedOk); + QFETCH(Architecture, expectedArchitecture); + QFETCH(Platform, expectedPlatform); + QFETCH(bool, expectedCompilerPresent); + QFETCH(Compiler, expectedCompiler); + QFETCH(bool, expectedPlatformVersionPresent); + QFETCH(QVersionNumber, expectedPlatformVersion); + QFETCH(QByteArray, expectedConverted); + + Architecture actualArchitecture{}; + Platform actualPlatform{}; + Compiler actualCompiler{}; + QVersionNumber actualPlatformVersion; + + const bool ok = clang::parseTriplet(triplet, &actualArchitecture, &actualPlatform, + &actualCompiler, &actualPlatformVersion); + QCOMPARE(ok, expectedOk); + if (ok) { + QCOMPARE(actualArchitecture, expectedArchitecture); + QCOMPARE(actualPlatform, expectedPlatform); + if (expectedPlatformVersionPresent) { + QCOMPARE(actualPlatformVersion.isNull(), expectedPlatformVersion.isNull()); + QCOMPARE(actualPlatformVersion, expectedPlatformVersion); + } else { + actualPlatformVersion = QVersionNumber{}; // clear host version + } + if (expectedCompilerPresent) + QCOMPARE(expectedCompiler, actualCompiler); + if (expectedOk) { + auto actualConverted = clang::targetTripletForPlatform(actualPlatform, actualArchitecture, + actualCompiler, actualPlatformVersion); + QCOMPARE(actualConverted, expectedConverted); + } + } +} + QTEST_APPLESS_MAIN(TestExtraInclude) diff --git a/sources/shiboken6/ApiExtractor/tests/testextrainclude.h b/sources/shiboken6/ApiExtractor/tests/testextrainclude.h index ae1a1868..8a2493aa 100644 --- a/sources/shiboken6/ApiExtractor/tests/testextrainclude.h +++ b/sources/shiboken6/ApiExtractor/tests/testextrainclude.h @@ -12,6 +12,8 @@ class TestExtraInclude : public QObject private slots: void testClassExtraInclude(); void testGlobalExtraIncludes(); + void testParseTriplet_data(); + void testParseTriplet(); }; #endif diff --git a/sources/shiboken6/ApiExtractor/tests/testmodifydocumentation.cpp b/sources/shiboken6/ApiExtractor/tests/testmodifydocumentation.cpp index c2fc3b21..9e59ebde 100644 --- a/sources/shiboken6/ApiExtractor/tests/testmodifydocumentation.cpp +++ b/sources/shiboken6/ApiExtractor/tests/testmodifydocumentation.cpp @@ -28,7 +28,7 @@ R"(
<brief>Modified Brief</brief> - <para>Some changed contents here</para> + <para>Some changed contents here</para> )"; @@ -66,8 +66,7 @@ R"( const char expectedDoc[] = R"( -oi -Paragraph number 1 +before briefParagraph number 1 Paragraph number 2 Some changed contents here diff --git a/sources/shiboken6/ApiExtractor/tests/testutil.h b/sources/shiboken6/ApiExtractor/tests/testutil.h index 8f79b4a7..40501c35 100644 --- a/sources/shiboken6/ApiExtractor/tests/testutil.h +++ b/sources/shiboken6/ApiExtractor/tests/testutil.h @@ -49,7 +49,7 @@ namespace TestUtil } QByteArrayList arguments; arguments.append(QFile::encodeName(tempSource.fileName())); - tempSource.write(cppCode, qint64(strlen(cppCode))); + tempSource.write(cppCode, qint64(qstrlen(cppCode))); tempSource.close(); auto builder = std::make_unique(); diff --git a/sources/shiboken6/ApiExtractor/typedatabase.cpp b/sources/shiboken6/ApiExtractor/typedatabase.cpp index 5cebd850..2e1f345c 100644 --- a/sources/shiboken6/ApiExtractor/typedatabase.cpp +++ b/sources/shiboken6/ApiExtractor/typedatabase.cpp @@ -6,6 +6,7 @@ #include "addedfunction.h" #include "messages.h" #include "typesystemparser_p.h" +#include "clangparser/compilersupport.h" #include "complextypeentry.h" #include "constantvaluetypeentry.h" #include "containertypeentry.h" @@ -433,10 +434,32 @@ void TypeDatabase::addRequiredTargetImport(const QString& moduleName) d->m_requiredTargetImports << moduleName; } +static QStringList platformKeywords() +{ + static constexpr auto unixKeyword = "unix"_L1; + static constexpr auto linuxKeyword = "linux"_L1; + switch (clang::platform()) { + case Platform::Unix: + return {unixKeyword}; + case Platform::Linux: + return {unixKeyword, linuxKeyword}; + case Platform::Windows: + return {"windows"_L1}; + case Platform::macOS: + return {unixKeyword, "darwin"_L1}; + case Platform::Android: + return {unixKeyword, linuxKeyword, "android"_L1}; + case Platform::iOS: + return {unixKeyword, "ios"_L1}; + } + return {}; +} + QStringList TypeDatabase::typesystemKeywords() const { - QStringList result = d->m_typesystemKeywords; - for (const auto &d : d->m_dropTypeEntries) + QStringList result = d->m_typesystemKeywords + platformKeywords(); + + for (const auto &d : std::as_const(d->m_dropTypeEntries)) result.append("no_"_L1 + d); switch (clang::emulatedCompilerLanguageLevel()) { diff --git a/sources/shiboken6/ApiExtractor/typesystem.cpp b/sources/shiboken6/ApiExtractor/typesystem.cpp index 0820150f..c02ec0c9 100644 --- a/sources/shiboken6/ApiExtractor/typesystem.cpp +++ b/sources/shiboken6/ApiExtractor/typesystem.cpp @@ -1225,7 +1225,9 @@ public: FlagsTypeEntryPtr m_flags; QString m_cppType; QString m_docFile; + std::weak_ptr m_aliasTypeEntry; TypeSystem::PythonEnumType m_pythonEnumType = TypeSystem::PythonEnumType::Unspecified; + EnumTypeEntry::AliasMode m_aliasMode = EnumTypeEntry::AliasMode::NoAlias; }; EnumTypeEntry::EnumTypeEntry(const QString &entryName, @@ -1330,6 +1332,30 @@ void EnumTypeEntry::setDocFile(const QString &df) d->m_docFile = df; } +EnumTypeEntry::AliasMode EnumTypeEntry::aliasMode() const +{ + S_D(const EnumTypeEntry); + return d->m_aliasMode; +} + +void EnumTypeEntry::setAliasMode(AliasMode am) +{ + S_D(EnumTypeEntry); + d->m_aliasMode = am; +} + +EnumTypeEntryCPtr EnumTypeEntry::aliasTypeEntry() const +{ + S_D(const EnumTypeEntry); + return d->m_aliasTypeEntry.lock(); +} + +void EnumTypeEntry::setAliasTypeEntry(const EnumTypeEntryCPtr &entry) +{ + S_D(EnumTypeEntry); + d->m_aliasTypeEntry = entry; +} + TypeEntry *EnumTypeEntry::clone() const { S_D(const EnumTypeEntry); @@ -2154,6 +2180,7 @@ public: QString m_resetMethod; SmartPointerTypeEntry::Instantiations m_instantiations; TypeEntryCList m_excludedInstantiations; + CustomConversionPtr m_customConversion; TypeSystem::SmartPointerType m_smartPointerType; }; @@ -2306,6 +2333,24 @@ QString SmartPointerTypeEntry::getTargetName(const AbstractMetaType &metaType) c return fixSmartPointerName(name); } +bool SmartPointerTypeEntry::hasCustomConversion() const +{ + S_D(const SmartPointerTypeEntry); + return bool(d->m_customConversion); +} + +void SmartPointerTypeEntry::setCustomConversion(const CustomConversionPtr &customConversion) +{ + S_D(SmartPointerTypeEntry); + d->m_customConversion = customConversion; +} + +CustomConversionPtr SmartPointerTypeEntry::customConversion() const +{ + S_D(const SmartPointerTypeEntry); + return d->m_customConversion; +} + // ----------------- NamespaceTypeEntry class NamespaceTypeEntryPrivate : public ComplexTypeEntryPrivate { diff --git a/sources/shiboken6/ApiExtractor/typesystem_enums.h b/sources/shiboken6/ApiExtractor/typesystem_enums.h index c0c3da1f..2b876efc 100644 --- a/sources/shiboken6/ApiExtractor/typesystem_enums.h +++ b/sources/shiboken6/ApiExtractor/typesystem_enums.h @@ -36,6 +36,7 @@ enum CodeSnipPosition { CodeSnipPositionEnd, CodeSnipPositionDeclaration, CodeSnipPositionPyOverride, + CodeSnipPositionWrapperDeclaration, CodeSnipPositionAny }; diff --git a/sources/shiboken6/ApiExtractor/typesystemparser.cpp b/sources/shiboken6/ApiExtractor/typesystemparser.cpp index 1d747419..e3a825e9 100644 --- a/sources/shiboken6/ApiExtractor/typesystemparser.cpp +++ b/sources/shiboken6/ApiExtractor/typesystemparser.cpp @@ -341,7 +341,8 @@ ENUM_LOOKUP_BEGIN(TypeSystem::CodeSnipPosition, Qt::CaseInsensitive, {u"beginning", TypeSystem::CodeSnipPositionBeginning}, {u"end", TypeSystem::CodeSnipPositionEnd}, {u"declaration", TypeSystem::CodeSnipPositionDeclaration}, - {u"override", TypeSystem::CodeSnipPositionPyOverride} + {u"override", TypeSystem::CodeSnipPositionPyOverride}, + {u"wrapper-declaration", TypeSystem::CodeSnipPositionWrapperDeclaration} }; ENUM_LOOKUP_LINEAR_SEARCH @@ -1178,10 +1179,10 @@ bool TypeSystemParser::importFileElement(const QXmlStreamAttributes &atts) } } if (!foundFromOk || !foundToOk) { - QString fromError = QString::fromLatin1("Could not find quote-after-line='%1' in file '%2'.") - .arg(quoteFrom.toString(), fileName); - QString toError = QString::fromLatin1("Could not find quote-before-line='%1' in file '%2'.") - .arg(quoteTo.toString(), fileName); + QString fromError = "Could not find quote-after-line='%1' in file '%2'."_L1 + .arg(quoteFrom.toString(), fileName); + QString toError = "Could not find quote-before-line='%1' in file '%2'."_L1 + .arg(quoteTo.toString(), fileName); if (!foundToOk) m_error = toError; @@ -2249,7 +2250,10 @@ TypeSystemTypeEntryPtr TypeSystemParser::parseRootElement(const ConditionalStrea if (m_defaultPackage.isEmpty()) { // Extending default, see addBuiltInContainerTypes() auto moduleEntry = std::const_pointer_cast(m_context->db->defaultTypeSystemType()); - Q_ASSERT(moduleEntry); + if (!moduleEntry) { + m_error = "No type system entry found (\"package\" attribute missing?)."_L1; + return {}; + } m_defaultPackage = moduleEntry->name(); return moduleEntry; } @@ -2347,9 +2351,10 @@ bool TypeSystemParser::parseCustomConversion(const ConditionalStreamReader &, if (topElement != StackElement::ModifyArgument && topElement != StackElement::ValueTypeEntry && topElement != StackElement::PrimitiveTypeEntry - && topElement != StackElement::ContainerTypeEntry) { + && topElement != StackElement::ContainerTypeEntry + && topElement != StackElement::SmartPointerTypeEntry) { m_error = u"Conversion rules can only be specified for argument modification, " - "value-type, primitive-type or container-type conversion."_s; + "value-type, primitive-type, or container-type or smartpointer-type conversion."_s; return false; } @@ -2414,6 +2419,9 @@ bool TypeSystemParser::parseCustomConversion(const ConditionalStreamReader &, std::static_pointer_cast(top->entry)->setCustomConversion(customConversion); else if (top->entry->isValue()) std::static_pointer_cast(top->entry)->setCustomConversion(customConversion); + else if (top->entry->isSmartPointer()) + std::static_pointer_cast(top->entry)->setCustomConversion(customConversion); + customConversionsForReview.append(customConversion); return true; } @@ -2479,7 +2487,7 @@ static bool parseIndex(const QString &index, int *result, QString *errorMessage) bool ok = false; *result = index.toInt(&ok); if (!ok) - *errorMessage = QString::fromLatin1("Cannot convert '%1' to integer").arg(index); + *errorMessage = "Cannot convert '%1' to integer"_L1.arg(index); return ok; } @@ -2691,8 +2699,8 @@ bool TypeSystemParser::parseAddFunction(const ConditionalStreamReader &, || topElement == StackElement::Root || topElement == StackElement::ContainerTypeEntry; if (!validParent) { - m_error = QString::fromLatin1("Add/Declare function requires a complex/container type or a root tag as parent" - ", was=%1").arg(tagFromElement(topElement)); + m_error = "Add/Declare function requires a complex/container type or a root tag as parent, was=%1"_L1 + + tagFromElement(topElement); return false; } @@ -2823,8 +2831,8 @@ bool TypeSystemParser::parseProperty(const ConditionalStreamReader &, StackEleme QXmlStreamAttributes *attributes) { if (!isComplexTypeEntry(topElement)) { - m_error = QString::fromLatin1("Add property requires a complex type as parent" - ", was=%1").arg(tagFromElement(topElement)); + m_error = "Add property requires a complex type as parent, was=%1"_L1 + + tagFromElement(topElement); return false; } @@ -2922,8 +2930,8 @@ bool TypeSystemParser::parseModifyFunction(const ConditionalStreamReader &reader || topElement == StackElement::TypedefTypeEntry || topElement == StackElement::FunctionTypeEntry; if (!validParent) { - m_error = QString::fromLatin1("Modify function requires complex type as parent" - ", was=%1").arg(tagFromElement(topElement)); + m_error = "Modify function requires complex type as parent, was=%1"_L1 + + tagFromElement(topElement); return false; } @@ -3590,6 +3598,8 @@ bool TypeSystemParser::startElement(const ConditionalStreamReader &reader, Stack switch (element) { case StackElement::Root: top->entry = parseRootElement(reader, versionRange.since, &attributes); + if (!top->entry) + return false; break; case StackElement::LoadTypesystem: if (!loadTypesystem(reader, &attributes)) diff --git a/sources/shiboken6/cmake/ShibokenHelpers.cmake b/sources/shiboken6/cmake/ShibokenHelpers.cmake index bcaf31e8..0e993fad 100644 --- a/sources/shiboken6/cmake/ShibokenHelpers.cmake +++ b/sources/shiboken6/cmake/ShibokenHelpers.cmake @@ -33,9 +33,13 @@ macro(set_debug_build) endmacro() macro(setup_sanitize_address) - # Currently this does not check that the clang / gcc version used supports Address sanitizer, - # so once again, use at your own risk. - add_compile_options("-fsanitize=address" "-g" "-fno-omit-frame-pointer") + # Currently this does not check that the clang / gcc / MSVC version used supports Address + # sanitizer, so once again, use at your own risk. + if(MSVC) + add_compile_options("/fsanitize=address") + else() + add_compile_options("-fsanitize=address" "-g" "-fno-omit-frame-pointer") + endif() # We need to add the sanitize address option to all linked executables / shared libraries # so that proper sanitizer symbols are linked in. # @@ -44,7 +48,21 @@ macro(setup_sanitize_address) # sanitizer will tell you what environment variable needs to be exported. For example: # export DYLD_INSERT_LIBRARIES=/Applications/Xcode.app/Contents/Developer/Toolchains/ # ./XcodeDefault.xctoolchain/usr/lib/clang/8.1.0/lib/darwin/libclang_rt.asan_osx_dynamic.dylib - set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_STANDARD_LIBRARIES} -fsanitize=address") + if(MSVC) + set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_STANDARD_LIBRARIES} /fsanitize=address") + else() + set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_STANDARD_LIBRARIES} -fsanitize=address") + endif() +endmacro() + +macro(setup_sanitize_thread) + if(MSVC) + set(sanitize_thread_option "/fsanitize=thread") + else() + set(sanitize_thread_option "-fsanitize=thread") + endif() + add_compile_options("${sanitize_thread_option}") + set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_STANDARD_LIBRARIES} ${sanitize_thread_option}") endmacro() macro(set_cmake_cxx_flags) @@ -347,7 +365,7 @@ macro(shiboken_find_required_python) Python ${_shiboken_find_python_version_args} REQUIRED - COMPONENTS Interpreter Development + COMPONENTS Interpreter Development.Module ) endif() @@ -869,28 +887,9 @@ endfunction() # Get path to libclang.dll/libclang.so depending on the platform macro(find_libclang) - if(CMAKE_HOST_WIN32) - set(libclang_directory_suffix "bin") - set(libclang_suffix ".dll") - else() - set(libclang_directory_suffix "lib") - if(CMAKE_HOST_APPLE) - set(libclang_suffix ".dylib") - else() - set(libclang_suffix ".so") - endif() - endif() - - set(libclang_lib_dir "") - if(DEFINED ENV{LLVM_INSTALL_DIR}) - set(libclang_lib_dir "$ENV{LLVM_INSTALL_DIR}/${libclang_directory_suffix}") - elseif(DEFINED ENV{CLANG_INSTALL_DIR}) - set(libclang_lib_dir "$ENV{CLANG_INSTALL_DIR}/${libclang_directory_suffix}") - else() - message(WARNING - "Couldn't find libclang${libclang_suffix} " - "You will likely need to add it manually to PATH to ensure the build succeeds.") - endif() + find_package(Clang CONFIG REQUIRED) + get_target_property(libclang_location libclang LOCATION) + get_filename_component(libclang_lib_dir "${libclang_location}" DIRECTORY) endmacro() # Allow setting a shiboken debug level from the the build system or from the environment diff --git a/sources/shiboken6/data/CMakeLists.txt b/sources/shiboken6/data/CMakeLists.txt index 45e04c9c..eb3c0e1f 100644 --- a/sources/shiboken6/data/CMakeLists.txt +++ b/sources/shiboken6/data/CMakeLists.txt @@ -10,30 +10,18 @@ endif() include(CMakePackageConfigHelpers) # Build-tree / super project package config file. -set(SHIBOKEN_PYTHON_MODULE_DIR "${shiboken6_BINARY_DIR}/shibokenmodule") -set(SHIBOKEN_SHARED_LIBRARY_DIR "${shiboken6_BINARY_DIR}/libshiboken") - configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/Shiboken6Config-spec.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/Shiboken6Config${PYTHON_CONFIG_SUFFIX}.cmake" INSTALL_DESTINATION "${CMAKE_CURRENT_BINARY_DIR}" - PATH_VARS SHIBOKEN_PYTHON_MODULE_DIR SHIBOKEN_SHARED_LIBRARY_DIR INSTALL_PREFIX "${CMAKE_CURRENT_BINARY_DIR}" ) # Install-tree / relocatable package config file. -set(SHIBOKEN_PYTHON_MODULE_DIR "${PYTHON_SITE_PACKAGES}/shiboken6") -if (WIN32) - set(SHIBOKEN_SHARED_LIBRARY_DIR "${BIN_INSTALL_DIR}") -else() - set(SHIBOKEN_SHARED_LIBRARY_DIR "${LIB_INSTALL_DIR}") -endif() - configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/Shiboken6Config-spec.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/install/Shiboken6Config${PYTHON_CONFIG_SUFFIX}.cmake" INSTALL_DESTINATION "${LIB_INSTALL_DIR}/cmake/Shiboken6" - PATH_VARS SHIBOKEN_PYTHON_MODULE_DIR SHIBOKEN_SHARED_LIBRARY_DIR ) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/Shiboken6Config.cmake.in" diff --git a/sources/shiboken6/data/Shiboken6Config-spec.cmake.in b/sources/shiboken6/data/Shiboken6Config-spec.cmake.in index 233404bc..57ad645a 100644 --- a/sources/shiboken6/data/Shiboken6Config-spec.cmake.in +++ b/sources/shiboken6/data/Shiboken6Config-spec.cmake.in @@ -35,7 +35,4 @@ set(SHIBOKEN_PYTHON_CONFIG_SUFFIX "@PYTHON_CONFIG_SUFFIX@") set(SHIBOKEN_SO_VERSION "@shiboken6_library_so_version@") set(SHIBOKEN_BUILD_TYPE "@SHIBOKEN_BUILD_TYPE@") -set_and_check(SHIBOKEN_PYTHON_MODULE_DIR "@PACKAGE_SHIBOKEN_PYTHON_MODULE_DIR@") -set_and_check(SHIBOKEN_SHARED_LIBRARY_DIR "@PACKAGE_SHIBOKEN_SHARED_LIBRARY_DIR@") - message(STATUS "libshiboken built for @SHIBOKEN_BUILD_TYPE@") diff --git a/sources/shiboken6/data/Shiboken6ToolsConfig.cmake.in b/sources/shiboken6/data/Shiboken6ToolsConfig.cmake.in index 438b5c65..fdc8e6dc 100644 --- a/sources/shiboken6/data/Shiboken6ToolsConfig.cmake.in +++ b/sources/shiboken6/data/Shiboken6ToolsConfig.cmake.in @@ -2,6 +2,29 @@ cmake_minimum_required(VERSION 3.18) +include(CMakeFindDependencyMacro) +if(NOT CMAKE_CROSSCOMPILING) + find_dependency(Python COMPONENTS Interpreter Development.Module) + + if(NOT SHIBOKEN6TOOLS_SKIP_FIND_DEPENDENCIES) + # Dynamically determine Python_SITELIB using Python itself + execute_process( + COMMAND ${Python_EXECUTABLE} -c + "import site; print(next(p for p in site.getsitepackages() if 'site-packages' in p))" + OUTPUT_VARIABLE Python_SITELIB + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + list(APPEND CMAKE_PREFIX_PATH + "${Python_SITELIB}/shiboken6/lib/cmake" + "${Python_SITELIB}/PySide6/lib/cmake" + ) + find_dependency(Shiboken6 REQUIRED) + find_dependency(PySide6 REQUIRED) + endif() +endif() + if(NOT TARGET Shiboken6::shiboken6) include("${CMAKE_CURRENT_LIST_DIR}/Shiboken6ToolsTargets.cmake") endif() + +include("${CMAKE_CURRENT_LIST_DIR}/Shiboken6ToolsMacros.cmake") diff --git a/sources/shiboken6/data/Shiboken6ToolsMacros.cmake b/sources/shiboken6/data/Shiboken6ToolsMacros.cmake new file mode 100644 index 00000000..b570e73c --- /dev/null +++ b/sources/shiboken6/data/Shiboken6ToolsMacros.cmake @@ -0,0 +1,187 @@ +# Function to configure a binding project +function(shiboken_generator_create_binding) + set(options FORCE_LIMITED_API) + set(one_value_args + EXTENSION_TARGET + TYPESYSTEM_FILE + LIBRARY_TARGET) + set(multi_value_args + GENERATED_SOURCES + HEADERS + QT_MODULES + SHIBOKEN_EXTRA_OPTIONS) + + cmake_parse_arguments(PARSE_ARGV 0 arg + "${options}" + "${one_value_args}" + "${multi_value_args}") + + # Validate required arguments + foreach(req EXTENSION_TARGET GENERATED_SOURCES HEADERS TYPESYSTEM_FILE LIBRARY_TARGET) + if(NOT DEFINED arg_${req}) + message(FATAL_ERROR "shiboken_generator_create_binding: ${req} is required") + endif() + endforeach() + + get_target_property(shiboken_include_dirs Shiboken6::libshiboken INTERFACE_INCLUDE_DIRECTORIES) + + # Get Shiboken path based on build type + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + get_target_property(shiboken_path Shiboken6::shiboken6 IMPORTED_LOCATION_DEBUG) + else() + get_target_property(shiboken_path Shiboken6::shiboken6 IMPORTED_LOCATION_RELEASE) + endif() + + # Basic shiboken options + set(shiboken_options + --generator-set=shiboken + --enable-parent-ctor-heuristic + --enable-return-value-heuristic + --use-isnull-as-nb_nonzero + --avoid-protected-hack + -I${CMAKE_CURRENT_SOURCE_DIR} + -T${CMAKE_CURRENT_SOURCE_DIR} + --output-directory=${CMAKE_CURRENT_BINARY_DIR}) + + # Add extra options if specified + if(arg_SHIBOKEN_EXTRA_OPTIONS) + list(APPEND shiboken_options ${arg_SHIBOKEN_EXTRA_OPTIONS}) + endif() + + # Add Qt/PySide specific configurations only if Qt modules are specified + if(arg_QT_MODULES) + # Get Qt include directories + set(qt_include_dirs "") + foreach(module ${arg_QT_MODULES}) + get_property(module_includes TARGET Qt6::${module} PROPERTY + INTERFACE_INCLUDE_DIRECTORIES) + list(APPEND qt_include_dirs ${module_includes}) + + # Check each module for framework on macOS + if(APPLE) + get_target_property(is_framework Qt6::${module} FRAMEWORK) + if(is_framework) + get_target_property(lib_location Qt6::${module} LOCATION) + get_filename_component(lib_dir "${lib_location}" DIRECTORY) + get_filename_component(framework_dir "${lib_dir}/../" ABSOLUTE) + list(APPEND shiboken_options "--framework-include-paths=${framework_dir}") + endif() + endif() + + # Add include paths to shiboken options + foreach(include_dir ${module_includes}) + list(APPEND shiboken_options "-I${include_dir}") + endforeach() + endforeach() + + get_target_property(pyside_include_dir PySide6::pyside6 INTERFACE_INCLUDE_DIRECTORIES) + + # Add PySide typesystems path + list(APPEND shiboken_options "-T${PYSIDE_TYPESYSTEMS}") + + # Enable PySide extensions + list(APPEND shiboken_options "--enable-pyside-extensions") + endif() + + # Generate binding sources + add_custom_command( + OUTPUT ${arg_GENERATED_SOURCES} + COMMAND "${shiboken_path}" + ${shiboken_options} ${arg_HEADERS} "${arg_TYPESYSTEM_FILE}" + DEPENDS ${arg_HEADERS} ${arg_TYPESYSTEM_FILE} + IMPLICIT_DEPENDS CXX ${arg_HEADERS} + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + COMMENT "Generating bindings for ${arg_EXTENSION_TARGET}" + ) + + # Create binding library + add_library(${arg_EXTENSION_TARGET} MODULE ${arg_GENERATED_SOURCES}) + + # set limited API + if(arg_FORCE_LIMITED_API OR FORCE_LIMITED_API) + target_compile_definitions(${arg_EXTENSION_TARGET} PRIVATE -DPy_LIMITED_API=0x03090000) + endif() + + + # Configure include paths + target_include_directories( + ${arg_EXTENSION_TARGET} PRIVATE + ${SHIBOKEN_PYTHON_INCLUDE_DIRS} + ${shiboken_include_dirs} + ${CMAKE_CURRENT_SOURCE_DIR} + ) + + # Link with Python, Shiboken and C++ library + target_link_libraries( + ${arg_EXTENSION_TARGET} PRIVATE + Shiboken6::libshiboken + ${arg_LIBRARY_TARGET} + ) + + if(arg_QT_MODULES) + # Add Qt and PySide includes + target_include_directories( + ${arg_EXTENSION_TARGET} PRIVATE ${qt_include_dirs} + ) + target_include_directories( + ${arg_EXTENSION_TARGET} PRIVATE ${pyside_include_dir} + ) + + # Add PySide Qt module-specific includes and link libraries + foreach(module ${arg_QT_MODULES}) + target_include_directories( + ${arg_EXTENSION_TARGET} PRIVATE "${pyside_include_dir}/Qt${module}" + ) + target_link_libraries( + ${arg_EXTENSION_TARGET} PRIVATE Qt6::${module} + ) + endforeach() + + # Link base PySide6 library + target_link_libraries( + ${arg_EXTENSION_TARGET} PRIVATE PySide6::pyside6 + ) + + # Link PySide6 QML library if Qml module is used + if("Qml" IN_LIST arg_QT_MODULES) + target_link_libraries( + ${arg_EXTENSION_TARGET} PRIVATE PySide6::pyside6qml + ) + endif() + endif() + + # Configure target properties + set_target_properties( + ${arg_EXTENSION_TARGET} PROPERTIES + PREFIX "" + OUTPUT_NAME "${arg_EXTENSION_TARGET}${SHIBOKEN_PYTHON_EXTENSION_SUFFIX}" + ) + + # Platform specific settings + if(WIN32) + # Add Python libraries only on Windows + get_property(SHIBOKEN_PYTHON_LIBRARIES GLOBAL PROPERTY shiboken_python_libraries) + + target_link_libraries( + ${arg_EXTENSION_TARGET} PRIVATE "${SHIBOKEN_PYTHON_LIBRARIES}" + ) + + # Set Windows-specific suffix + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set_property( + TARGET ${arg_EXTENSION_TARGET} PROPERTY SUFFIX "_d.pyd" + ) + else() + set_property( + TARGET ${arg_EXTENSION_TARGET} PROPERTY SUFFIX ".pyd" + ) + endif() + endif() + + if(APPLE) + set_target_properties( + ${arg_EXTENSION_TARGET} PROPERTIES + LINK_FLAGS "-undefined dynamic_lookup" + ) + endif() +endfunction() diff --git a/sources/shiboken6/data/shiboken6.pc.in b/sources/shiboken6/data/shiboken6.pc.in index a82d2316..917b706a 100644 --- a/sources/shiboken6/data/shiboken6.pc.in +++ b/sources/shiboken6/data/shiboken6.pc.in @@ -1,7 +1,7 @@ prefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=@CMAKE_INSTALL_PREFIX@ libdir=@CMAKE_INSTALL_PREFIX@/@LIB_INSTALL_DIR@ -includedir=@CMAKE_INSTALL_PREFIX@/include/shiboken6 +includedir=@CMAKE_INSTALL_PREFIX@/shiboken6/include python_interpreter=@Python_EXECUTABLE@ python_include_dir=@Python_INCLUDE_DIRS@ diff --git a/sources/shiboken6/doc/shibokengenerator.rst b/sources/shiboken6/doc/shibokengenerator.rst index 31df11b8..ce6a0612 100644 --- a/sources/shiboken6/doc/shibokengenerator.rst +++ b/sources/shiboken6/doc/shibokengenerator.rst @@ -202,14 +202,37 @@ Options When '-' is passed as the first option in the list, none of the options built into shiboken will be added, allowing for a complete replacement. +.. _compiler-option: + ``--compiler=`` Emulated compiler type (g++, msvc, clang) +.. _compiler-path-option: + ``--compiler-path=`` Path to the compiler for determining builtin include paths -``--platform=`` - Emulated platform (windows, darwin, unix) +.. _compiler-argument-option: + +``compiler-argument=`` + Add an argument for the compiler for determining builtin include paths + +.. _platform-option: + +``--platform=`` + Emulated platform (``android``, ``darwin``, ``ios``, ``linux``, ``unix``, ``windows``). + ``CMAKE_SYSTEM_NAME`` may be used. + +.. _platform-version-option: + +``--platform-version=`` + Platform version + +.. _arch-option: + +``--arch=`` + Emulated architecture (``x86_64``, ``arm64``, ``i586``). + ``CMAKE_SYSTEM_PROCESSOR`` may be used. .. _include-paths: @@ -390,3 +413,131 @@ becomes .. code-block:: ini VALUE-ARGUMENT = VALUE + + +.. _cross-compilation: + +Cross Compilation +================= + +Shiboken uses **libclang** to parse the headers of the library to be exposed. +When compiling for another platform, the clang parser should ideally use the +target of the platform. + +Simple bindings may already work when the parser uses the default host platform +target. But for bigger projects like Qt, it is important that macros like +``QT_POINTER_SIZE`` and the platform defines ``Q_OS_XXX`` are set correctly +when parsing files like ``qsystemdetection.h`` or ``qprocessordetection.h``. +Some Qt API might be excluded depending on platform and there might be subtle +differences depending on word size. + +For platform and architecture, the relevant command line options are +:ref:`platform-option` and :ref:`arch-option`. They take common platform names +and architectures as used in target triplets and can be set to the values of +the CMake variables ``CMAKE_SYSTEM_NAME`` and ``CMAKE_SYSTEM_PROCESSOR``, +respectively. If the specified platform is different from the host, Shiboken +will pass a target triplet based on them to the clang parser. + +Optionally, the version of the platform can be specified using the +:ref:`platform-version-option`. This is useful when the clang parser defaults +to a too-old version. + +If this results in a wrong or too generic triplet, it is also possible to +directly pass a target triplet in the Clang options specified by +:ref:`clang_option`. In this case, Shiboken will not pass a target triplet and +try to derive the platform/architecture from this triplet. + +When using the ``Clang`` and ``GNU`` compilers for cross-compiling, the +:ref:`compiler-path-option` option should be specified since Shiboken may need +to run the compiler to determine system include paths. For most cases, passing +the value of the CMake variable ``CMAKE_CXX_COMPILER`` should work. If the +compiler is in the path, it should suffice to pass the compiler type to +:ref:`compiler-option` (value of ``CMAKE_CXX_COMPILER_ID``). + +It is possible (for example, when targeting Android) that ``CMAKE_CXX_COMPILER`` +is a generic compiler that also needs a ``--target=`` or similar option to +locate the correct system include paths. In this case (shiboken failing due to +not finding some system headers), the :ref:`compiler-argument-option` can be +passed to specify the target. + +Typically, a ``CMakeLists.txt`` files will then look like: + +.. code-block:: cmake + + if (CMAKE_CROSSCOMPILING) + list(APPEND shiboken_command "--platform=${CMAKE_SYSTEM_NAME}" + "--arch=${CMAKE_SYSTEM_PROCESSOR}" + "--compiler-path=${CMAKE_CXX_COMPILER}") + endif() + +When passing the target triplet: + +.. code-block:: cmake + + if (CMAKE_CROSSCOMPILING) + list(APPEND shiboken_command "--clang-option=--target=aarch64-none-linux-android" + "--compiler-path=${CMAKE_CXX_COMPILER}") + endif() + +*********** +CMake Usage +*********** + +The ``Shiboken6Tools`` CMake package provides an easy way to invoke the +Shiboken generator from CMake to create Python bindings for C++ libraries. It +is contained in the ``shiboken6_generator`` wheel. This is achieved using the +``shiboken_generator_create_binding`` CMake function. This function automates +the process of generating binding sources and building the Python extension +module. + +Function Signature +================== + +.. code-block:: cmake + + shiboken_generator_create_binding( + TARGET_NAME + GENERATED_SOURCES + HEADERS + TYPESYSTEM_FILE + CPP_LIBRARY + [QT_MODULES ] + [EXTRA_OPTIONS ] + [FORCE_LIMITED_API] + ) + +Arguments +********* + +* ``TARGET_NAME``: Name of the Python extension module target to create. +* ``GENERATED_SOURCES``: List of C++ source files generated by Shiboken. +* ``HEADERS``: List of C++ header files to parse. +* ``TYPESYSTEM_FILE``: Path to the typesystem XML file. +* ``CPP_LIBRARY``: C++ library to link against. +* ``QT_MODULES`` (optional): List of Qt modules required for the binding. +* ``EXTRA_OPTIONS`` (optional): Additional command line options for Shiboken. +* ``FORCE_LIMITED_API`` (optional): Use the Limited API for the generated extension module. + +Usage Example +************* + +.. code-block:: cmake + + shiboken_generator_create_binding( + TARGET_NAME MyBinding + GENERATED_SOURCES ${generated_sources} + HEADERS ${wrapped_header} + TYPESYSTEM_FILE ${typesystem_file} + CPP_LIBRARY ${my_library} + QT_MODULES Core Gui Widgets + EXTRA_OPTIONS --some-extra-option + FORCE_LIMITED_API + ) + +This macro will generate the binding sources, build the Python module, and link it with the specified +libraries and include paths. + +For complete usage examples, see: + +* `SampleBinding Example `_ +* `WidgetBinding Example `_ diff --git a/sources/shiboken6/doc/typesystem_arguments.rst b/sources/shiboken6/doc/typesystem_arguments.rst index 90387ec1..4e54446c 100644 --- a/sources/shiboken6/doc/typesystem_arguments.rst +++ b/sources/shiboken6/doc/typesystem_arguments.rst @@ -192,8 +192,8 @@ holds the reference(s). It defaults to the function signature. For instance, in a model/view relation, a view receiving a model as argument for a **setModel()** method should increment the model's reference counting, since the model should be kept alive as long as the view lives. -Remember that our hypothetical view cannot become a :ref:`parent` of the -model, since the said model could be used by other views as well. +Remember that our hypothetical view cannot become a :ref:`parent ` +of the model, since the said model could be used by other views as well. .. _parent-on-arguments: diff --git a/sources/shiboken6/doc/typesystem_codeinjection.rst b/sources/shiboken6/doc/typesystem_codeinjection.rst index 0e047f38..d8b740d7 100644 --- a/sources/shiboken6/doc/typesystem_codeinjection.rst +++ b/sources/shiboken6/doc/typesystem_codeinjection.rst @@ -29,80 +29,83 @@ into the **C++ Wrapper** or the **Python Wrapper** (see The ``position`` attribute specifies the location of the custom code in the function. - -+---------------+------+-----------+--------------------------------------------------------------+ -|Parent Tag |Class |Position |Meaning | -+===============+======+===========+==============================================================+ -|value-type, |native|beginning |Write to the beginning of a class wrapper ``.cpp`` file, right| -|object-type | | |after the ``#include`` clauses. A common use would be to write| -| | | |prototypes for custom functions whose definitions are put on a| -| | | |``native/end`` code injection. | -| | +-----------+--------------------------------------------------------------+ -| | |end |Write to the end of a class wrapper ``.cpp`` file. Could be | -| | | |used to write custom/helper functions definitions for | -| | | |prototypes declared on ``native/beginning``. | -| +------+-----------+--------------------------------------------------------------+ -| |target|beginning |Put custom code on the beginning of the wrapper initializer | -| | | |function (``init_CLASS(PyObject *module)``). This could be | -| | | |used to manipulate the ``PyCLASS_Type`` structure before | -| | | |registering it on Python. | -| | +-----------+--------------------------------------------------------------+ -| | |end |Write the given custom code at the end of the class wrapper | -| | | |initializer function (``init_CLASS(PyObject *module)``). The | -| | | |code here will be executed after all the wrapped class | -| | | |components have been initialized. | -+---------------+------+-----------+--------------------------------------------------------------+ -|modify-function|native|beginning |Code here is put on the virtual method override of a C++ | -| | | |wrapper class (the one responsible for passing C++ calls to a | -| | | |Python override, if there is any), right after the C++ | -| | | |arguments have been converted but before the Python call. | -| | +-----------+--------------------------------------------------------------+ -| | |end |This code injection is put in a virtual method override on the| -| | | |C++ wrapper class, after the call to Python and before | -| | | |dereferencing the Python method and tuple of arguments. | -| +------+-----------+--------------------------------------------------------------+ -| |target|beginning |This code is injected on the Python method wrapper | -| | | |(``PyCLASS_METHOD(...)``), right after the decisor have found | -| | | |which signature to call and also after the conversion of the | -| | | |arguments to be used, but before the actual call. | -| | +-----------+--------------------------------------------------------------+ -| | |end |This code is injected on the Python method wrapper | -| | | |(``PyCLASS_METHOD(...)``), right after the C++ method call, | -| | | |but still inside the scope created by the overload for each | -| | | |signature. | -| +------+-----------+--------------------------------------------------------------+ -| |shell |declaration|Used only for virtual functions. This code is injected at the | -| | | |top. | -| | +-----------+--------------------------------------------------------------+ -| | |override |Used only for virtual functions. The code is injected before | -| | | |the code calling the Python override. | -| | +-----------+--------------------------------------------------------------+ -| | |beginning |Used only for virtual functions. The code is injected when the| -| | | |function does not has a Python implementation, then the code | -| | | |is inserted before c++ call | -| | +-----------+--------------------------------------------------------------+ -| | |end |Same as above, but the code is inserted after c++ call | -+---------------+------+-----------+--------------------------------------------------------------+ -|typesystem |native|beginning |Write code to the beginning of the module ``.cpp`` file, right| -| | | |after the ``#include`` clauses. This position has a similar | -| | | |purpose as the ``native/beginning`` position on a wrapper | -| | | |class ``.cpp`` file, namely write function prototypes, but not| -| | | |restricted to this use. | -| | +-----------+--------------------------------------------------------------+ -| | |end |Write code to the end of the module ``.cpp`` file. Usually | -| | | |implementations for function prototypes inserted at the | -| | | |beginning of the file with a ``native/beginning`` code | -| | | |injection. | -| +------+-----------+--------------------------------------------------------------+ -| |target|beginning |Insert code at the start of the module initialization function| -| | | |(``initMODULENAME()``), before the calling ``Py_InitModule``. | -| | +-----------+--------------------------------------------------------------+ -| | |end |Insert code at the end of the module initialization function | -| | | |(``initMODULENAME()``), but before the checking that emits a | -| | | |fatal error in case of problems importing the module. | -| | +-----------+--------------------------------------------------------------+ -| | |declaration|Insert code into module header. | -+---------------+------+-----------+--------------------------------------------------------------+ ++---------------+------+---------------------+--------------------------------------------------------------+ +|Parent Tag |Class |Position |Meaning | ++===============+======+=====================+==============================================================+ +|value-type, |native|beginning |Write to the beginning of a class wrapper ``.cpp`` file, right| +|object-type | | |after the ``#include`` clauses. A common use would be to write| +| | | |prototypes for custom functions whose definitions are put on a| +| | | |``native/end`` code injection. | +| | +---------------------+--------------------------------------------------------------+ +| | |end |Write to the end of a class wrapper ``.cpp`` file. Could be | +| | | |used to write custom/helper functions definitions for | +| | | |prototypes declared on ``native/beginning``. | +| | +---------------------+--------------------------------------------------------------+ +| | | wrapper-declaration |Write into the declaration of the wrapper class, right after | +| | | |the ``public:`` keyword. This can be used for importing base | +| | | |class members via ``using``. | +| +------+---------------------+--------------------------------------------------------------+ +| |target|beginning |Put custom code on the beginning of the wrapper initializer | +| | | |function (``init_CLASS(PyObject *module)``). This could be | +| | | |used to manipulate the ``PyCLASS_Type`` structure before | +| | | |registering it on Python. | +| | +---------------------+--------------------------------------------------------------+ +| | |end |Write the given custom code at the end of the class wrapper | +| | | |initializer function (``init_CLASS(PyObject *module)``). The | +| | | |code here will be executed after all the wrapped class | +| | | |components have been initialized. | ++---------------+------+---------------------+--------------------------------------------------------------+ +|modify-function|native|beginning |Code here is put on the virtual method override of a C++ | +| | | |wrapper class (the one responsible for passing C++ calls to a | +| | | |Python override, if there is any), right after the C++ | +| | | |arguments have been converted but before the Python call. | +| | +---------------------+--------------------------------------------------------------+ +| | |end |This code injection is put in a virtual method override on the| +| | | |C++ wrapper class, after the call to Python and before | +| | | |dereferencing the Python method and tuple of arguments. | +| +------+---------------------+--------------------------------------------------------------+ +| |target|beginning |This code is injected on the Python method wrapper | +| | | |(``PyCLASS_METHOD(...)``), right after the decisor have found | +| | | |which signature to call and also after the conversion of the | +| | | |arguments to be used, but before the actual call. | +| | +---------------------+--------------------------------------------------------------+ +| | |end |This code is injected on the Python method wrapper | +| | | |(``PyCLASS_METHOD(...)``), right after the C++ method call, | +| | | |but still inside the scope created by the overload for each | +| | | |signature. | +| +------+---------------------+--------------------------------------------------------------+ +| |shell |declaration |Used only for virtual functions. This code is injected at the | +| | | |top. | +| | +---------------------+--------------------------------------------------------------+ +| | |override |Used only for virtual functions. The code is injected before | +| | | |the code calling the Python override. | +| | +---------------------+--------------------------------------------------------------+ +| | |beginning |Used only for virtual functions. The code is injected when the| +| | | |function does not has a Python implementation, then the code | +| | | |is inserted before c++ call | +| | +---------------------+--------------------------------------------------------------+ +| | |end |Same as above, but the code is inserted after c++ call | ++---------------+------+---------------------+--------------------------------------------------------------+ +|typesystem |native|beginning |Write code to the beginning of the module ``.cpp`` file, right| +| | | |after the ``#include`` clauses. This position has a similar | +| | | |purpose as the ``native/beginning`` position on a wrapper | +| | | |class ``.cpp`` file, namely write function prototypes, but not| +| | | |restricted to this use. | +| | +---------------------+--------------------------------------------------------------+ +| | |end |Write code to the end of the module ``.cpp`` file. Usually | +| | | |implementations for function prototypes inserted at the | +| | | |beginning of the file with a ``native/beginning`` code | +| | | |injection. | +| +------+---------------------+--------------------------------------------------------------+ +| |target|beginning |Insert code at the start of the module initialization function| +| | | |(``initMODULENAME()``), before the calling ``Py_InitModule``. | +| | +---------------------+--------------------------------------------------------------+ +| | |end |Insert code at the end of the module initialization function | +| | | |(``initMODULENAME()``), but before the checking that emits a | +| | | |fatal error in case of problems importing the module. | +| | +---------------------+--------------------------------------------------------------+ +| | |declaration |Insert code into module header. | ++---------------+------+---------------------+--------------------------------------------------------------+ Anatomy of Code Injection @@ -335,7 +338,7 @@ Code injections to the class Python initialization function. return; Py_INCREF(&PyInjectCode_Type); - PyModule_AddObject(module, "InjectCode", + PyModule_Add(module, "InjectCode", ((PyObject*)&PyInjectCode_Type)); // INJECT-CODE: diff --git a/sources/shiboken6/doc/typesystem_conversionrule.rst b/sources/shiboken6/doc/typesystem_conversionrule.rst index cee45bfb..f6ce1834 100644 --- a/sources/shiboken6/doc/typesystem_conversionrule.rst +++ b/sources/shiboken6/doc/typesystem_conversionrule.rst @@ -12,9 +12,13 @@ The **conversion-rule** tag specifies how a **primitive-type**, a **container-ty or a **value-type** may be converted to and from the native C++ language types to the target language types (see also :ref:`user-defined-type-conversion`). -It is a child of the :ref:`container-type`, :ref:`primitive-type` or -:ref:`value-type` and may contain :ref:`native-to-target` or -:ref:`native-to-target` child nodes. +It may be a child of the :ref:`container-type` and :ref:`primitive-type` nodes, +where conversions have to be provided for both directions using the +:ref:`native-to-target` and :ref:`target-to-native` child nodes. + +It may also appear as a child of :ref:`value-type` or :ref:`smart-pointer-type` +where additional conversions from other target language types can be provided +using the :ref:`target-to-native` child node. .. code-block:: xml @@ -70,8 +74,9 @@ an input value an does what's needed to convert it to the output value.
Use the replace node to modify the template code. -Notice that the generator must provide type system variables for the input -and output values and types, namely **%in**, **%out**, **%INTYPE** and +Notice that the generator provides type system variables for the input +and output values and types (see :ref:`converter_variables_and_functions`). +The most important ones are **%in**, **%out**, **%INTYPE** and **%OUTTYPE**. In the case of container types, **%INTYPE** refers to the full container type (e.g. **"list"**) and **%INTYPE_0**, **%INTYPE_1**, **%INTYPE_#**, should be replaced by the types used in the container template diff --git a/sources/shiboken6/doc/typesystem_specifying_types.rst b/sources/shiboken6/doc/typesystem_specifying_types.rst index c03d203b..7fe4df39 100644 --- a/sources/shiboken6/doc/typesystem_specifying_types.rst +++ b/sources/shiboken6/doc/typesystem_specifying_types.rst @@ -208,6 +208,9 @@ can be generated for them. Instead, an instance of the viewed class should be instantiated and passed to functions using the view class for argument types. +It is also possible to specify template specializations +like "std::optional" as primitive types with converters. + See :ref:`predefined_templates` for built-in templates for standard type conversion rules. @@ -344,6 +347,23 @@ production of ABI compatible bindings. The **flags-revision** attribute has the same purposes of **revision** attribute but is used for the QFlag related to this enum. +An enum can also be a C++ type alias: + +.. code-block:: c++ + + enum Option { Value1 = 0; } + + class SomeClass { + public: + using OptionAlias = Option; + }; + +In this case, when specifying `` in +`SomeClass`, an enumeration `OptionAlias` will be generated into the class. The +values of `OptionAlias` and `Option` can be used interchangeably. This feature +is specifically intended for renaming enumerations by deprecating; it works for +at most one alias. + .. _reject-enum-value: reject-enum-value @@ -726,7 +746,8 @@ The ``smart pointer`` type node indicates that the given class is a smart pointe and requires inserting calls to **getter** to access the pointeee. Currently, the usage is limited to function return values. **ref-count-method** specifies the name of the method used to do reference counting. -It is a child of the :ref:`typesystem_details` node or other type nodes. +It is a child of the :ref:`typesystem_details` node or other type nodes +and may contain :ref:`conversion-rule` nodes. The *optional* attribute **instantiations** specifies for which instantiations of the smart pointer wrappers will be generated (comma-separated list). diff --git a/sources/shiboken6/doc/typesystem_variables.rst b/sources/shiboken6/doc/typesystem_variables.rst index 6dfd1f80..179c97ad 100644 --- a/sources/shiboken6/doc/typesystem_variables.rst +++ b/sources/shiboken6/doc/typesystem_variables.rst @@ -218,14 +218,14 @@ conversion code (see :ref:`converter_variables_and_functions`). .. code-block:: c++ - long a = PyLong_AS_LONG(%PYARG_1); + long a = PyLong_AsLong(%PYARG_1); is equivalent of .. code-block:: c++ - long a = PyLong_AS_LONG(PyTuple_GetItem(%PYTHON_ARGUMENTS, 0)); + long a = PyLong_AsLong(PyTuple_GetItem(%PYTHON_ARGUMENTS, 0)); The generator tries to be smart with attributions, but it will work for the diff --git a/sources/shiboken6/generator/CMakeLists.txt b/sources/shiboken6/generator/CMakeLists.txt index 454e9cf7..997468f0 100644 --- a/sources/shiboken6/generator/CMakeLists.txt +++ b/sources/shiboken6/generator/CMakeLists.txt @@ -69,6 +69,18 @@ install(EXPORT "${package_name}Targets" NAMESPACE "Shiboken6::" DESTINATION ${LIB_INSTALL_DIR}/cmake/${package_name}) +# Add wheel specific installation +if(NOT is_pyside6_superproject_build) + install(TARGETS shiboken6 + EXPORT "${package_name}WheelTargets" + DESTINATION "shiboken6_generator") + + install(EXPORT "${package_name}WheelTargets" + NAMESPACE "Shiboken6::" + DESTINATION "${LIB_INSTALL_DIR}/wheels/cmake/${package_name}" + FILE "${package_name}Targets.cmake") +endif() + set(shiboken_generator_package_name "shiboken6_generator") configure_file("${CMAKE_CURRENT_SOURCE_DIR}/_config.py.in" @@ -101,6 +113,11 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/_git_shiboken_generator_version.py" include(CMakePackageConfigHelpers) +# Copy macros file to build tree +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/../data/${package_name}Macros.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${package_name}Macros.cmake" + COPYONLY) + # Single build-tree and install-tree Config file. There's no need for separate ones because we # don't specify any PATH_VARS, so the relative path of PACKAGE_PREFIX_DIR does not really matter. configure_package_config_file( @@ -115,8 +132,9 @@ write_basic_package_version_file( ARCH_INDEPENDENT ) -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${package_name}Config.cmake" - DESTINATION "${LIB_INSTALL_DIR}/cmake/${package_name}") - -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${package_name}ConfigVersion.cmake" - DESTINATION "${LIB_INSTALL_DIR}/cmake/${package_name}") +# Install the config files +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/${package_name}Config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${package_name}ConfigVersion.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${package_name}Macros.cmake" + DESTINATION "${LIB_INSTALL_DIR}/cmake/${package_name}") diff --git a/sources/shiboken6/generator/generator.cpp b/sources/shiboken6/generator/generator.cpp index 808234a7..5b04e738 100644 --- a/sources/shiboken6/generator/generator.cpp +++ b/sources/shiboken6/generator/generator.cpp @@ -370,7 +370,7 @@ QString Generator::getFullTypeName(const AbstractMetaType &type) typeName = getFullTypeNameWithoutModifiers(type); else typeName = getFullTypeName(type.typeEntry()); - return typeName + QString::fromLatin1("*").repeated(type.indirections()); + return typeName + QString(type.indirections(), u'*'); } QString Generator::getFullTypeName(const AbstractMetaClassCPtr &metaClass) diff --git a/sources/shiboken6/generator/main.cpp b/sources/shiboken6/generator/main.cpp index 80047d7c..3fd5383d 100644 --- a/sources/shiboken6/generator/main.cpp +++ b/sources/shiboken6/generator/main.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -85,11 +86,19 @@ OptionDescriptions CommonOptionsParser::optionDescriptions() {u"documentation-only"_s, u"Do not generates any code, just the documentation"_s}, {u"compiler="_s, - u"Emulated compiler type (g++, msvc, clang)"_s}, + u"Emulated compiler type (g++/gnu, msvc, clang). CMAKE_CXX_COMPILER_ID may be used."_s}, {u"platform="_s, - u"Emulated platform (windows, darwin, unix)"_s}, + u"Emulated platform (android, darwin, ios, linux, unix, windows)." + " CMAKE_SYSTEM_NAME may be used."_s}, + {u"platform-version="_s, + u"Platform version"_s}, + {u"arch="_s, + u"Emulated architecture (x86_64, arm64, i586)." + " CMAKE_SYSTEM_PROCESSOR may be used."_s}, {u"compiler-path="_s, u"Path to the compiler for determining builtin include paths"_s}, + {u"compiler-argument="_s, + u"Add an argument for the compiler for determining builtin include paths"_s}, {u"generator-set=<\"generator module\">"_s, u"generator-set to be used. e.g. qtdoc"_s}, {u"diff"_s, u"Print a diff of wrapper files"_s}, @@ -185,17 +194,40 @@ bool CommonOptionsParser::handleOption(const QString &key, const QString &value, return true; } if (key == u"compiler") { - if (!clang::setCompiler(value)) - throw Exception(u"Invalid value \""_s + value + u"\" passed to --compiler"_s); + if (!clang::setCompiler(value)) { + qCWarning(lcShiboken, "Invalid compiler \"%s\" passed to --compiler, defaulting to host.", + qPrintable(value)); + } return true; } if (key == u"compiler-path") { clang::setCompilerPath(value); return true; } + if (key == u"compiler-argument") { + clang::addCompilerArgument(value); + return true; + } + if (key == u"platform") { - if (!clang::setPlatform(value)) - throw Exception(u"Invalid value \""_s + value + u"\" passed to --platform"_s); + if (!clang::setPlatform(value)) { + qCWarning(lcShiboken, "Invalid value \"%s\" passed to --platform, defaulting to host.", + qPrintable(value)); + } + return true; + } + + if (key == u"platform-version") { + if (!clang::setPlatformVersion(value)) + throw Exception("Invalid value "_L1 + value + " passed to --platform-version."_L1); + return true; + } + + if (key == u"arch") { + if (!clang::setArchitecture(value)) { + qCWarning(lcShiboken, "Invalid architecture \"%s\" passed to --arch defaulting to host.", + qPrintable(value)); + } return true; } @@ -436,5 +468,7 @@ int wmain(int argc, wchar_t *argv[]) std::cerr << appName << " error: " << e.what() << '\n'; ex = EXIT_FAILURE; } + if (ex != 0 && qEnvironmentVariableIsSet("COIN_UNIQUE_JOB_ID")) + ReportHandler::dumpGeneralLogFile(); return ex; } diff --git a/sources/shiboken6/generator/qtdoc/qtdocgenerator.cpp b/sources/shiboken6/generator/qtdoc/qtdocgenerator.cpp index 7cd28be1..3875c53f 100644 --- a/sources/shiboken6/generator/qtdoc/qtdocgenerator.cpp +++ b/sources/shiboken6/generator/qtdoc/qtdocgenerator.cpp @@ -492,6 +492,40 @@ void QtDocGenerator::generateClassRecursion(TextStream &s, const QString &target } } +void QtDocGenerator::writeDetailedDescription(TextStream &s, + const AbstractMetaClassCPtr &metaClass, + const QString &scope, + QtXmlToSphinxImages *parsedImages) const +{ + auto documentation = metaClass->documentation(); + writeInjectDocumentation(s, TypeSystem::DocModificationPrepend, metaClass, + parsedImages); + if (!writeInjectDocumentation(s, TypeSystem::DocModificationReplace, metaClass, + parsedImages)) + writeFormattedDetailedText(s, documentation, scope, parsedImages); + writeInjectDocumentation(s, TypeSystem::DocModificationAppend, metaClass, + parsedImages); +} + +enum ClassDescriptionMode +{ + NoDescription, + BriefOnly, + DetailedOnly, + BriefAndDetailed, + BriefAndDetailedSections, +}; + +static ClassDescriptionMode classDescriptionMode(const Documentation &doc) +{ + if (!doc.hasDetailed()) + return doc.hasBrief() ? BriefOnly : NoDescription; + if (!doc.hasBrief()) + return DetailedOnly; + return doc.detailed().contains("documentation(); const QString scope = classScope(metaClass); - if (documentation.hasBrief()) + + const auto descriptionMode = classDescriptionMode(documentation); + switch (descriptionMode) { + case NoDescription: + case DetailedOnly: + break; + case BriefOnly: writeFormattedBriefText(s, documentation, scope, &parsedImages); + break; + case BriefAndDetailed: { + // A "collapse" sphinx directive can be used for brief/expanding to details + // for descriptions consisting of a paragraph sequence. + writeFormattedBriefText(s, documentation, scope, &parsedImages); + s << "\n\n.. collapse:: Details\n\n"; + Indentation detailIndent(s); + writeDetailedDescription(s, metaClass, scope, &parsedImages); + } + break; + case BriefAndDetailedSections: { + // If the the description has nested
's (which break collapse::), we + // use a 'more' label for the detailed text to be written further down. + QString brief = documentation.brief(); + brief.insert(brief.lastIndexOf(u'<'), " More_..."_L1); + writeFormattedText(s, brief, documentation.format(), scope, &parsedImages); + } + break; + } if (!metaClass->baseClasses().isEmpty()) { if (m_options.inheritanceDiagram) { @@ -545,13 +604,17 @@ void QtDocGenerator::doGenerateClass(TextStream &s, const QString &targetDir, " translation, you can also let us know by creating a ticket on\n" " https:/bugreports.qt.io/projects/PYSIDE\n\n"; - s << '\n' << headline("Detailed Description") << ".. _More:\n"; - - writeInjectDocumentation(s, TypeSystem::DocModificationPrepend, metaClass, - &parsedImages); - if (!writeInjectDocumentation(s, TypeSystem::DocModificationReplace, metaClass, &parsedImages)) - writeFormattedDetailedText(s, documentation, scope, &parsedImages); - writeInjectDocumentation(s, TypeSystem::DocModificationAppend, metaClass, &parsedImages); + switch (descriptionMode) { + case DetailedOnly: + case BriefAndDetailedSections: + s << '\n' << headline("Detailed Description"); + if (descriptionMode == BriefAndDetailedSections) + s << ".. _More:\n"; + writeDetailedDescription(s, metaClass, scope, &parsedImages); + break; + default: + break; + } writeEnums(s, metaClass->enums(), scope, &parsedImages); @@ -901,8 +964,8 @@ QString QtDocGenerator::translateToPythonType(const AbstractMetaType &type, strType.remove(u"QHash"_s); strType.remove(u"QMap"_s); QStringList types = strType.split(u','); - strType = QString::fromLatin1("Dictionary with keys of type %1 and values of type %2.") - .arg(types[0], types[1]); + strType = "Dictionary with keys of type %1 and values of type %2."_L1 + .arg(types[0], types[1]); } return strType; } diff --git a/sources/shiboken6/generator/qtdoc/qtdocgenerator.h b/sources/shiboken6/generator/qtdoc/qtdocgenerator.h index 8937814a..ac5e22f3 100644 --- a/sources/shiboken6/generator/qtdoc/qtdocgenerator.h +++ b/sources/shiboken6/generator/qtdoc/qtdocgenerator.h @@ -69,6 +69,9 @@ private: QList *contexts); void doGenerateClass(TextStream &ts, const QString &targetDir, const AbstractMetaClassCPtr &metaClass); + void writeDetailedDescription(TextStream &s, + const AbstractMetaClassCPtr &metaClass, const QString &scope, + QtXmlToSphinxImages *parsedImages) const; void writeEnums(TextStream &s, const AbstractMetaEnumList &enums, const QString &scope, QtXmlToSphinxImages *images) const; diff --git a/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp index bddf2f51..3ffe80f3 100644 --- a/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp +++ b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp @@ -18,6 +18,8 @@ #include #include +#include + using namespace Qt::StringLiterals; QDebug operator<<(QDebug debug, const QtXmlToSphinxImage &i) @@ -198,9 +200,9 @@ enum class WebXmlTag { heading, brief, para, italic, bold, see_also, snippet, dots, codeline, table, header, row, item, argument, teletype, link, inlineimage, image, list, term, raw, underline, superscript, code, badcode, legalese, - rst, section, quotefile, + rst, section, quotefile, target, keyword, page, group, // ignored tags - generatedlist, tableofcontents, quotefromfile, skipto, target, page, group, + generatedlist, tableofcontents, quotefromfile, skipto, // useless tags description, definition, printuntil, relation, // Doxygen tags @@ -251,6 +253,7 @@ static const WebXmlTagHash &webXmlTagHash() {u"quotefromfile", WebXmlTag::quotefromfile}, {u"skipto", WebXmlTag::skipto}, {u"target", WebXmlTag::target}, + {u"keyword", WebXmlTag::keyword}, {u"page", WebXmlTag::page}, {u"group", WebXmlTag::group}, {u"description", WebXmlTag::description}, @@ -401,6 +404,7 @@ void QtXmlToSphinx::callHandler(WebXmlTag t, QXmlStreamReader &r) handleIgnoredTag(r); break; case WebXmlTag::target: + case WebXmlTag::keyword: handleTargetTag(r); break; case WebXmlTag::page: @@ -688,15 +692,13 @@ QString QtXmlToSphinx::readSnippet(const QString &location, const QString &ident void QtXmlToSphinx::handleHeadingTag(QXmlStreamReader& reader) { static int headingSize = 0; - static char type; - static char types[] = { '-', '^' }; + static char type{}; + static constexpr const char types[] = R"(#*=-^")"; QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { - uint typeIdx = reader.attributes().value(u"level"_s).toUInt(); - if (typeIdx >= sizeof(types)) - type = types[sizeof(types)-1]; - else - type = types[typeIdx]; + // Levels are 1..n. We start at #2 since already uses '#' (1) for the title. + const auto typeIdx = std::size_t(reader.attributes().value(u"level"_s).toUInt()); // level 1..n + type = types[std::min(typeIdx, std::strlen(types) - 1)]; } else if (token == QXmlStreamReader::EndElement) { m_output << disableIndent << Pad(type, headingSize) << "\n\n" << enableIndent; @@ -1305,7 +1307,7 @@ void QtXmlToSphinx::handlePageTag(QXmlStreamReader &reader) ? writeEscapedRstText(m_output, title) : writeEscapedRstText(m_output, fullTitle); - m_output << '\n' << Pad('*', size) << "\n\n" + m_output << '\n' << Pad('#', size) << "\n\n" << enableIndent; } @@ -1315,7 +1317,7 @@ void QtXmlToSphinx::handleTargetTag(QXmlStreamReader &reader) return; const auto name = reader.attributes().value("name"); if (!name.isEmpty()) - m_output << rstLabel(name.toString()); + m_output << disableIndent << rstLabel(name.toString()) << enableIndent; } void QtXmlToSphinx::handleIgnoredTag(QXmlStreamReader&) diff --git a/sources/shiboken6/generator/shiboken/cppgenerator.cpp b/sources/shiboken6/generator/shiboken/cppgenerator.cpp index 654b5021..36f34de5 100644 --- a/sources/shiboken6/generator/shiboken/cppgenerator.cpp +++ b/sources/shiboken6/generator/shiboken/cppgenerator.cpp @@ -88,6 +88,23 @@ TextStream &operator<<(TextStream &str, const sbkUnusedVariableCast &c) return str; } +struct retrieveWrapper +{ + explicit retrieveWrapper(const AbstractMetaClassCPtr &klass, + QAnyStringView instanceName = "this") + : m_klass(klass), m_instanceName(instanceName) {} + + const AbstractMetaClassCPtr m_klass; + const QAnyStringView m_instanceName; +}; + +TextStream &operator<<(TextStream &str, const retrieveWrapper &rw) +{ + str << "Shiboken::BindingManager::instance().retrieveWrapper(" << rw.m_instanceName + << ", " << CppGenerator::cpythonTypeName(rw.m_klass) << ')'; + return str; +} + struct pyTypeGetSlot { explicit pyTypeGetSlot(QAnyStringView funcType, QAnyStringView typeObject, @@ -336,15 +353,6 @@ static QString compilerOptionOptimize() return result; } -QString CppGenerator::chopType(QString s) -{ - if (s.endsWith(u"_Type")) - s.chop(5); - else if (s.endsWith(u"_TypeF()")) - s.chop(8); - return s; -} - static bool isStdSetterName(const QString &setterName, const QString &propertyName) { return setterName.size() == propertyName.size() + 3 @@ -474,10 +482,17 @@ static QString BuildEnumFlagInfo(const AbstractMetaEnum &cppEnum) } static void writePyGetSetDefEntry(TextStream &s, const QString &name, - const QString &getFunc, const QString &setFunc) + const QString &getFunc, const QString &setFunc, const QString &doc={}) { - s << "{const_cast(\"" << mangleName(name) << "\"), " << getFunc << ", " - << (setFunc.isEmpty() ? NULL_PTR : setFunc) << ", nullptr, nullptr},\n"; + s << "{\"" << mangleName(name) << "\", " << getFunc << ", " + << (setFunc.isEmpty() ? NULL_PTR : setFunc) << ", "; + + if (doc.isEmpty()) + s << "nullptr"; + else + s << "\"" << doc << "\""; + + s << ", nullptr},\n"; } static bool generateRichComparison(const GeneratorContext &c) @@ -511,7 +526,7 @@ void CppGenerator::generateIncludes(TextStream &s, const GeneratorContext &class "type_traits"}; // enum/underlying type // headers s << "// default includes\n"; - s << "#include \n"; + s << "#include \n#include \n#include \n"; if (wrapperDiagnostics()) { s << "#include \n"; cppIncludes << "iostream"; @@ -702,6 +717,8 @@ void CppGenerator::generateClass(TextStream &s, s << '\n'; + writeClassTypeFunction(s, classContext.metaClass()); + // class inject-code native/beginning if (!typeEntry->codeSnips().isEmpty()) { writeClassCodeSnips(s, typeEntry->codeSnips(), @@ -812,7 +829,7 @@ void CppGenerator::generateClass(TextStream &s, const QString methodsDefinitions = md.toString(); const QString singleMethodDefinitions = smd.toString(); - const QString className = chopType(cpythonTypeName(metaClass)); + const QString className = cpythonBaseName(metaClass); // Write single method definitions s << singleMethodDefinitions; @@ -913,8 +930,9 @@ void CppGenerator::generateClass(TextStream &s, const QString setter = canGenerateSetter ? cpythonSetterFunctionName(metaField) : QString(); const auto names = metaField.definitionNames(); + const QString doc = metaField.type().pythonSignature(); for (const auto &name : names) - writePyGetSetDefEntry(s, name, getter, setter); + writePyGetSetDefEntry(s, name, getter, setter, doc); } } @@ -978,7 +996,7 @@ void CppGenerator::writeCacheResetNative(TextStream &s, const GeneratorContext & { s << "void " << classContext.wrapperName() << "::resetPyMethodCache()\n{\n" << indent - << "std::fill_n(m_PyMethodCache, sizeof(m_PyMethodCache) / sizeof(m_PyMethodCache[0]), false);\n" + << "std::fill(m_PyMethodCache.begin(), m_PyMethodCache.end(), nullptr);\n" << outdent << "}\n\n"; } @@ -1014,9 +1032,8 @@ void CppGenerator::writeDestructorNative(TextStream &s, if (wrapperDiagnostics()) s << R"(std::cerr << __FUNCTION__ << ' ' << this << '\n';)" << '\n'; // kill pyobject - s << R"(SbkObject *wrapper = Shiboken::BindingManager::instance().retrieveWrapper(this); -Shiboken::Object::destroy(wrapper, this); -)" << outdent << "}\n"; + s << "auto *wrapper = " << retrieveWrapper(classContext.metaClass()) + << ";\nShiboken::Object::destroy(wrapper, this);\n" << outdent << "}\n"; } // Return type for error messages when getting invalid types from virtual @@ -1062,8 +1079,8 @@ QString CppGenerator::getVirtualFunctionReturnTypeName(const AbstractMetaFunctio if (func->type().isPrimitive()) return u'"' + func->type().name() + u'"'; - return u"Shiboken::SbkType< "_s - + typeEntry->qualifiedCppName() + u" >()->tp_name"_s; + return u"PepType_GetFullyQualifiedNameStr(Shiboken::SbkType< "_s + + typeEntry->qualifiedCppName() + u" >())"_s; } void CppGenerator::writeVirtualMethodCppCall(TextStream &s, @@ -1296,7 +1313,8 @@ void CppGenerator::writeVirtualMethodNative(TextStream &s, const QString funcName = func->isOperatorOverload() ? pythonOperatorFunctionName(func) : func->definitionNames().constFirst(); - QString className = wrapperName(func->ownerClass()); + auto owner = func->ownerClass(); + QString className = wrapperName(owner); const Options options = Generator::SkipDefaultValues | Generator::OriginalTypeDescription; s << functionSignature(func, className, {}, options) << "\n{\n" << indent; @@ -1338,7 +1356,8 @@ void CppGenerator::writeVirtualMethodNative(TextStream &s, s << "static PyObject *nameCache[2] = {};\n" << "Shiboken::GilState gil(false);\n" << "Shiboken::AutoDecRef " << PYTHON_OVERRIDE_VAR << "(Sbk_GetPyOverride(" - << "this, gil, funcName, &m_PyMethodCache[" << cacheIndex << "], nameCache));\n" + << "this, " << CppGenerator::cpythonTypeName(owner) << ", gil, funcName, m_PyMethodCache[" + << cacheIndex << "], nameCache));\n" << "if (pyOverride.isNull()) {\n" << indent; writeVirtualMethodCppCall(s, func, funcName, snips, lastArg, retType, returnStatement.statement, false, true); @@ -1352,7 +1371,6 @@ void CppGenerator::writeVirtualMethodNative(TextStream &s, if (!func->isVoid()) s << "return "; - auto owner = func->ownerClass(); const auto &reusedFuncs = getReusedOverridenFunctions(owner); auto rit = reusedFuncs.constFind(func); const bool canReuse = rit != reusedFuncs.cend(); @@ -1436,7 +1454,7 @@ void CppGenerator::writeVirtualMethodPythonOverride(TextStream &s, if (!snips.isEmpty()) { if (func->injectedCodeUsesPySelf()) - s << "PyObject *pySelf = BindingManager::instance().retrieveWrapper(this);\n"; + s << "PyObject *pySelf = " << retrieveWrapper(func->ownerClass()) << ";\n"; const AbstractMetaArgument *lastArg = func->arguments().isEmpty() ? nullptr : &func->arguments().constLast(); @@ -1656,10 +1674,11 @@ void CppGenerator::writeMetaCast(TextStream &s, const QString qualifiedCppName = classContext.metaClass()->qualifiedCppName(); s << "void *" << wrapperClassName << "::qt_metacast(const char *_clname)\n{\n" << indent << "if (_clname == nullptr)\n" << indent << "return {};\n" << outdent - << "SbkObject *pySelf = Shiboken::BindingManager::instance().retrieveWrapper(this);\n" - << "if (pySelf != nullptr && PySide::inherits(Py_TYPE(pySelf), _clname))\n" + << "if (SbkObject *pySelf = Shiboken::BindingManager::instance().retrieveWrapper(this)) {\n" << indent + << "auto *obSelf = reinterpret_cast(pySelf);\n" + << "if (PySide::inherits(Py_TYPE(obSelf), _clname))\n" << indent << "return static_cast(const_cast< " - << wrapperClassName << " *>(this));\n" << outdent + << wrapperClassName << " *>(this));\n" << outdent << outdent << "}\n" << "return " << qualifiedCppName << "::qt_metacast(_clname);\n" << outdent << "}\n\n"; } @@ -1717,6 +1736,17 @@ void CppGenerator::writeEnumConverterFunctions(TextStream &s, const AbstractMeta writePythonToCppFunction(s, c.toString(), enumConverterPythonType, typeName); QString pyTypeCheck = u"PyObject_TypeCheck(pyIn, "_s + enumPythonType + u')'; + switch (metaEnum.typeEntry()->aliasMode()) { + case EnumTypeEntry::NoAlias: + break; + case EnumTypeEntry::AliasSource: + case EnumTypeEntry::AliasTarget: { + const QString &aliasSourceType = cpythonTypeNameExt(metaEnum.typeEntry()->aliasTypeEntry()); + pyTypeCheck += "\n || PyObject_TypeCheck(pyIn, "_L1 + aliasSourceType + u')'; + } + break; + } + writeIsPythonConvertibleToCppFunction(s, enumConverterPythonType, typeName, pyTypeCheck); c.clear(); @@ -1724,9 +1754,9 @@ void CppGenerator::writeEnumConverterFunctions(TextStream &s, const AbstractMeta c << "using IntType = std::underlying_type_t<" << cppTypeName << ">;\n" "const auto castCppIn = IntType(*reinterpret_cast(cppIn));\n" << "return " - << "Shiboken::Enum::newItem(" << enumPythonType << ", castCppIn);\n"; + << "Shiboken::Enum::newItem(pyType, castCppIn);\n"; s << '\n'; - writeCppToPythonFunction(s, c.toString(), typeName, enumConverterPythonType); + writeCppToPythonFunction(s, c.toString(), typeName, enumConverterPythonType, true); s << '\n'; auto flags = enumType->flags(); @@ -1753,7 +1783,7 @@ static void writePointerToPythonConverter(TextStream &c, const QString &typeName, const QString &cpythonType) { - c << "auto *pyOut = reinterpret_cast(Shiboken::BindingManager::instance().retrieveWrapper(cppIn));\n" + c << "auto *pyOut = reinterpret_cast(" << retrieveWrapper(metaClass, "cppIn") << ");\n" << "if (pyOut) {\n" << indent << "Py_INCREF(pyOut);\nreturn pyOut;\n" << outdent << "}\n"; @@ -2101,6 +2131,24 @@ void CppGenerator::writeCustomConverterRegister(TextStream &s, } } +void CppGenerator::writeTemplateCustomConverterRegister(TextStream &s, + const AbstractMetaType &type, + QString converter) +{ + auto customConversion = CustomConversion::getCustomConversion(type.typeEntry()); + if (!customConversion || customConversion->targetToNativeConversions().isEmpty()) + return; + if (converter.isEmpty()) + converter = converterVar; + const QString typeName = fixedCppTypeName(type); + for (const auto &conv : customConversion->targetToNativeConversions()) { + const QString &sourceTypeName = conv.sourceTypeName(); + QString toCpp = pythonToCppFunctionName(sourceTypeName, typeName); + QString isConv = convertibleToCppFunctionName(sourceTypeName, typeName); + writeAddPythonToCppConversion(s, converter, toCpp, isConv); + } +} + void CppGenerator::writeContainerConverterFunctions(TextStream &s, const AbstractMetaType &containerType) const { @@ -2315,12 +2363,8 @@ void CppGenerator::writeConstructorWrapper(TextStream &s, const OverloadData &ov // Python owns it and C++ wrapper is false. if (shouldGenerateCppWrapper(overloadData.referenceFunction()->ownerClass())) s << "Shiboken::Object::setHasCppWrapper(sbkSelf, true);\n"; - // Need to check if a wrapper for same pointer is already registered - // Caused by bug PYSIDE-217, where deleted objects' wrappers are not released - s << "if (Shiboken::BindingManager::instance().hasWrapper(cptr)) {\n" << indent - << "Shiboken::BindingManager::instance().releaseWrapper(" - "Shiboken::BindingManager::instance().retrieveWrapper(cptr));\n" << outdent - << "}\nShiboken::BindingManager::instance().registerWrapper(sbkSelf, cptr);\n"; + + s << "Shiboken::BindingManager::instance().registerWrapper(sbkSelf, cptr);\n"; // Create metaObject and register signal/slot if (needsMetaObject) { @@ -3389,15 +3433,17 @@ QString CppGenerator::convertibleToCppFunctionName(const TargetToNativeConversio } void CppGenerator::writeCppToPythonFunction(TextStream &s, const QString &code, const QString &sourceTypeName, - const QString &targetTypeName) const + const QString &targetTypeName, bool withType) const { QString prettyCode = code; const QString funcName = cppToPythonFunctionName(sourceTypeName, targetTypeName); processCodeSnip(prettyCode, funcName); - s << "static PyObject *" << funcName - << "(const void *cppIn)\n{\n" << indent << prettyCode + s << "static PyObject *" << funcName <<'('; + if (withType) + s << "PyTypeObject *pyType, "; + s << "const void *cppIn)\n{\n" << indent << prettyCode << ensureEndl << outdent << "}\n"; } @@ -3502,9 +3548,16 @@ void CppGenerator::writeIsPythonConvertibleToCppFunction(TextStream &s, if (!condition.contains(u"pyIn")) s << sbkUnusedVariableCast("pyIn"); } - s << "if (" << condition << ")\n" << indent - << "return " << pythonToCppFuncName << ";\n" << outdent - << "return {};\n" << outdent << "}\n"; + + const bool useBrace = condition.contains(u'\n'); + s << "if (" << condition << ')'; + if (useBrace) + s<< " {"; + s << '\n' << indent + << "return " << pythonToCppFuncName << ";\n" << outdent; + if (useBrace) + s<< "}\n"; + s << "return {};\n" << outdent << "}\n"; } void CppGenerator::writePythonToCppConversionFunctions(TextStream &s, @@ -3562,14 +3615,7 @@ void CppGenerator::writePythonToCppConversionFunctions(TextStream &s, writePythonToCppFunction(s, code, sourceTypeName, targetTypeName); // Python to C++ convertible check function. - QString typeCheck = toNative.sourceTypeCheck(); - if (typeCheck.isEmpty()) { - QString pyTypeName = toNative.sourceTypeName(); - if (pyTypeName == u"Py_None" || pyTypeName == u"PyNone") - typeCheck = u"%in == Py_None"_s; - else if (pyTypeName == u"SbkObject") - typeCheck = u"Shiboken::Object::checkType(%in)"_s; - } + QString typeCheck = toNative.sourceTypeCheckFallback(); if (typeCheck.isEmpty()) { if (!toNative.sourceType() || toNative.sourceType()->isPrimitive()) { QString m; @@ -3580,33 +3626,47 @@ void CppGenerator::writePythonToCppConversionFunctions(TextStream &s, typeCheck = u"PyObject_TypeCheck(%in, "_s + cpythonTypeNameExt(toNative.sourceType()) + u')'; } - typeCheck.replace(u"%in"_s, u"pyIn"_s); - processCodeSnip(typeCheck, targetType->qualifiedCppName()); + processTypeCheckCodeSnip(typeCheck, targetType->qualifiedCppName()); writeIsPythonConvertibleToCppFunction(s, sourceTypeName, targetTypeName, typeCheck); } -void CppGenerator::writePythonToCppConversionFunctions(TextStream &s, const AbstractMetaType &containerType) const +void CppGenerator::writePythonToCppConversionFunctions(TextStream &s, + const AbstractMetaType &templateType) const { - Q_ASSERT(containerType.typeEntry()->isContainer()); - const auto cte = std::static_pointer_cast(containerType.typeEntry()); - const auto customConversion = cte->customConversion(); - for (const auto &conv : customConversion->targetToNativeConversions()) - writePythonToCppConversionFunction(s, containerType, conv); + const auto customConversion = CustomConversion::getCustomConversion(templateType.typeEntry()); + if (customConversion) { + const auto &conversions = customConversion->targetToNativeConversions(); + for (const auto &conv : conversions) + writePythonToCppConversionFunction(s, templateType, conv); + } } void CppGenerator::writePythonToCppConversionFunction(TextStream &s, - const AbstractMetaType &containerType, + const AbstractMetaType &templateType, const TargetToNativeConversion &conv) const { + // Python to C++ convertible check function. + QString typeName = fixedCppTypeName(templateType); + // Check fallback is too broad for containers that need elements of same type + QString typeCheck = templateType.isContainer() + ? conv.sourceTypeCheck() : conv.sourceTypeCheckFallback(); + if (typeCheck.isEmpty()) { + typeCheck = cpythonCheckFunction(templateType); + if (typeCheck.isEmpty()) + typeCheck = u"false"_s; + else + typeCheck = typeCheck + u"pyIn)"_s; + } + // Python to C++ conversion function. - QString cppTypeName = getFullTypeNameWithoutModifiers(containerType); + QString cppTypeName = getFullTypeNameWithoutModifiers(templateType); QString code = conv.conversion(); const QString line = u"auto &cppOutRef = *reinterpret_cast<"_s + cppTypeName + u" *>(cppOut);"_s; CodeSnipAbstract::prependCode(&code, line); - for (qsizetype i = 0; i < containerType.instantiations().size(); ++i) { - const AbstractMetaType &type = containerType.instantiations().at(i); - QString typeName = getFullTypeName(type); + for (qsizetype i = 0; i < templateType.instantiations().size(); ++i) { + const AbstractMetaType &type = templateType.instantiations().at(i); + QString instTypeName = getFullTypeName(type); // Containers of opaque containers are not handled here. const auto generatorArg = GeneratorArgument::fromMetaType(type); if (generatorArg.indirections > 0 && !type.generateOpaqueContainer()) { @@ -3620,23 +3680,19 @@ void CppGenerator::writePythonToCppConversionFunction(TextStream &s, rightCode.replace(varName, u'*' + varName); code.replace(pos, code.size() - pos, rightCode); } - typeName.append(u" *"_s); + instTypeName.append(" *"_L1); } - code.replace(u"%OUTTYPE_"_s + QString::number(i), typeName); + const QString var = "%OUTTYPE_"_L1 + QString::number(i); + code.replace(var, instTypeName); + typeCheck.replace(var, instTypeName); } code.replace(u"%OUTTYPE"_s, cppTypeName); code.replace(u"%in"_s, u"pyIn"_s); code.replace(u"%out"_s, u"cppOutRef"_s); - QString typeName = fixedCppTypeName(containerType); const QString &sourceTypeName = conv.sourceTypeName(); writePythonToCppFunction(s, code, sourceTypeName, typeName); - // Python to C++ convertible check function. - QString typeCheck = cpythonCheckFunction(containerType); - if (typeCheck.isEmpty()) - typeCheck = u"false"_s; - else - typeCheck = typeCheck + u"pyIn)"_s; + processTypeCheckCodeSnip(typeCheck, typeName); // needs %OUTTYPE_[n] writeIsPythonConvertibleToCppFunction(s, sourceTypeName, typeName, typeCheck); s << '\n'; } @@ -4423,8 +4479,7 @@ QString CppGenerator::writeContainerConverterInitialization(TextStream &s, s << '&' << targetTypeName << "_Type"; } - const QString typeName = fixedCppTypeName(type); - s << ", " << cppToPythonFunctionName(typeName, targetTypeName) << ");\n"; + s << ", " << cppToPythonFunctionName(fixedCppTypeName(type), targetTypeName) << ");\n"; s << registerConverterName(cppSignature, converter); if (usePySideExtensions() && cppSignature.startsWith("const "_L1) @@ -4433,12 +4488,7 @@ QString CppGenerator::writeContainerConverterInitialization(TextStream &s, s << registerConverterName(underlyingType, converter); } - for (const auto &conv : typeEntry->customConversion()->targetToNativeConversions()) { - const QString &sourceTypeName = conv.sourceTypeName(); - QString toCpp = pythonToCppFunctionName(sourceTypeName, typeName); - QString isConv = convertibleToCppFunctionName(sourceTypeName, typeName); - writeAddPythonToCppConversion(s, converter, toCpp, isConv); - } + writeTemplateCustomConverterRegister(s, type, converter); auto typedefItPair = api.typedefTargetToName().equal_range(type.cppSignature()); if (typedefItPair.first != typedefItPair.second) { @@ -4570,6 +4620,17 @@ static QString docString(const AbstractMetaClassCPtr &metaClass) return it != docModifs.cend() ? it->code().trimmed() : QString{}; } +void CppGenerator::writeClassTypeFunction(TextStream &s, + const AbstractMetaClassCPtr &metaClass) +{ + const QString className = cpythonBaseName(metaClass); + const QString typePtr = u"_"_s + className + u"_Type"_s; + s << openExternC << "static PyTypeObject *" << typePtr << " = nullptr;\n" + << "static PyTypeObject *" << className << "_TypeF(void)\n" + << "{\n" << indent << "return " << typePtr << ";\n" << outdent << "}\n" + << closeExternC; +} + void CppGenerator::writeClassDefinition(TextStream &s, const AbstractMetaClassCPtr &metaClass, const GeneratorContext &classContext) @@ -4578,7 +4639,7 @@ void CppGenerator::writeClassDefinition(TextStream &s, QString tp_dealloc; QString tp_hash; QString tp_call; - const QString className = chopType(cpythonTypeName(metaClass)); + const QString className = cpythonBaseName(metaClass); bool onlyPrivCtor = !metaClass->hasNonPrivateConstructor(); @@ -4664,8 +4725,8 @@ void CppGenerator::writeClassDefinition(TextStream &s, s << '\n'; } - s << "// Class Definition -----------------------------------------------\n" - "extern \"C\" {\n"; + s << "\n// Class Definition -----------------------------------------------\n" + << openExternC; if (hasHashFunction(metaClass)) tp_hash = u'&' + cpythonBaseName(metaClass) + u"_HashFunc"_s; @@ -4674,12 +4735,7 @@ void CppGenerator::writeClassDefinition(TextStream &s, if (callOp && !callOp->isModifiedRemoved()) tp_call = u'&' + cpythonFunctionName(callOp); - const QString typePtr = u"_"_s + className - + u"_Type"_s; - s << "static PyTypeObject *" << typePtr << " = nullptr;\n" - << "static PyTypeObject *" << className << "_TypeF(void)\n" - << "{\n" << indent << "return " << typePtr << ";\n" << outdent - << "}\n\nstatic PyType_Slot " << className << "_slots[] = {\n" << indent + s << "\nstatic PyType_Slot " << className << "_slots[] = {\n" << indent << "{Py_tp_base, nullptr}, // inserted by introduceWrapperType\n" << pyTypeSlotEntry("Py_tp_dealloc", tp_dealloc) << pyTypeSlotEntry("Py_tp_repr", m_tpFuncs.value(REPR_FUNCTION)) @@ -4938,7 +4994,7 @@ QString CppGenerator::writeCopyFunction(TextStream &s, const GeneratorContext &context) { const auto &metaClass = context.metaClass(); - const QString className = chopType(cpythonTypeName(metaClass)); + const QString className = cpythonBaseName(metaClass); const QString funcName = className + u"__copy__"_s; // PYSIDE-3135 replace _Self by Self when the minimum Python version is 3.11 @@ -5018,27 +5074,7 @@ void CppGenerator::writeGetterFunction(TextStream &s, cppField = u"cppOut_local"_s; } - s << "PyObject *pyOut = {};\n"; if (newWrapperSameObject) { - // Special case colocated field with same address (first field in a struct) - s << "if (reinterpret_cast(" - << cppField << ") == reinterpret_cast(" - << CPP_SELF_VAR << ")) {\n" << indent - << "pyOut = reinterpret_cast(Shiboken::Object::findColocatedChild(" - << "reinterpret_cast(self), " - << cpythonTypeNameExt(fieldType) << "));\n" - << "if (pyOut != nullptr) {\n" << indent - << "Py_IncRef(pyOut);\n" - << "return pyOut;\n" - << outdent << "}\n"; - // Check if field wrapper has already been created. - s << outdent << "} else if (Shiboken::BindingManager::instance().hasWrapper(" - << cppField << ")) {" << "\n" << indent - << "pyOut = reinterpret_cast(Shiboken::BindingManager::instance().retrieveWrapper(" - << cppField << "));" << "\n" - << "Py_IncRef(pyOut);" << "\n" - << "return pyOut;" << "\n" - << outdent << "}\n"; // Create and register new wrapper. We force a pointer conversion also // for wrapped value types so that they refer to the struct member, // avoiding any trouble copying them. Add a parent relationship to @@ -5047,15 +5083,23 @@ void CppGenerator::writeGetterFunction(TextStream &s, // unsolved issues when using temporary Python lists of structs // which can cause elements to be reported deleted in expressions like // "foo.list_of_structs[2].field". - s << "pyOut = " - << "Shiboken::Object::newObject(" << cpythonTypeNameExt(fieldType) - << ", " << cppField << ", false, true);\n" - << "Shiboken::Object::setParent(self, pyOut)"; + s << "PyObject *pyOut = {};\n" + << "auto *fieldTypeObject = " << cpythonTypeNameExt(fieldType) << ";\n" + << "if (auto *sbkOut = Shiboken::BindingManager::instance().retrieveWrapper(" + << cppField << ", fieldTypeObject)) {\n" << indent + << "pyOut = reinterpret_cast(sbkOut);\n" + << "Py_INCREF(pyOut);\n" << outdent << "} else {\n" << indent + << "pyOut = Shiboken::Object::newObject(fieldTypeObject, " + << cppField << ", false, true);\n" + << "Shiboken::Object::setParent(self, pyOut);\n" + << outdent << "}\n" + << "return pyOut;\n"; } else { - s << "pyOut = "; + s << "return "; writeToPythonConversion(s, fieldType, metaField.enclosingClass(), cppField); + s << ";\n"; } - s << ";\nreturn pyOut;\n" << outdent << "}\n"; + s << outdent << "}\n"; } // Write a getter for QPropertySpec @@ -5448,14 +5492,14 @@ void CppGenerator::writeSignatureInfo(TextStream &s, const OverloadData &overloa } } -void CppGenerator::writeEnumsInitialization(TextStream &s, AbstractMetaEnumList &enums) +void CppGenerator::writeEnumsInitialization(TextStream &s, const AbstractMetaEnumList &enums) { if (enums.isEmpty()) return; bool preambleWritten = false; bool etypeUsed = false; - for (const AbstractMetaEnum &cppEnum : std::as_const(enums)) { + for (const AbstractMetaEnum &cppEnum : enums) { if (cppEnum.isPrivate()) continue; if (!preambleWritten) { @@ -5471,6 +5515,14 @@ void CppGenerator::writeEnumsInitialization(TextStream &s, AbstractMetaEnumList s << sbkUnusedVariableCast("EType"); } +void CppGenerator::writeEnumsInitFunc(TextStream &s, const QString &funcName, + const AbstractMetaEnumList &enums) +{ + s << "static void " << funcName << "(PyObject *module)\n{\n" << indent; + writeEnumsInitialization(s, enums); + s << outdent << "}\n\n"; +} + static qsizetype maxLineLength(const QStringList &list) { qsizetype result = 0; @@ -5583,7 +5635,7 @@ bool CppGenerator::writeEnumInitialization(TextStream &s, const AbstractMetaEnum << indent << (isSigned ? "PyLong_FromLongLong" : "PyLong_FromUnsignedLongLong") << "(" << pyValue << "));\n" << outdent; } else { - s << "PyModule_AddObject(module, \"" << mangledName << "\",\n" << indent + s << "PepModule_Add(module, \"" << mangledName << "\",\n" << indent << (isSigned ? "PyLong_FromLongLong" : "PyLong_FromUnsignedLongLong") << "(" << pyValue << "));\n" << outdent; } @@ -5812,7 +5864,7 @@ void CppGenerator::writeClassRegister(TextStream &s, AbstractMetaClassCPtr enc = metaClass->targetLangEnclosingClass(); QString enclosingObjectVariable = enc ? u"enclosingClass"_s : u"module"_s; - QString pyTypeName = cpythonTypeName(metaClass); + QString pyTypePrefix = cpythonBaseName(metaClass); QString initFunctionName = getInitFunctionName(classContext); // PYSIDE-510: Create a signatures string for the introspection feature. @@ -5827,7 +5879,7 @@ void CppGenerator::writeClassRegister(TextStream &s, << "return " << globalTypeVarExpr << ";\n\n" << outdent; // Multiple inheritance - QString pyTypeBasesVariable = chopType(pyTypeName) + u"_Type_bases"_s; + QString pyTypeBasesVariable = pyTypePrefix + u"_Type_bases"_s; const QStringList pyBases = pyBaseTypes(metaClass); s << "Shiboken::AutoDecRef " << pyTypeBasesVariable << "(PyTuple_Pack(" << pyBases.size() << ",\n" << indent; @@ -5839,8 +5891,7 @@ void CppGenerator::writeClassRegister(TextStream &s, s << "));\n\n" << outdent; // Create type and insert it in the module or enclosing class. - const QString typePtr = u"_"_s + chopType(pyTypeName) - + u"_Type"_s; + const QString typePtr = u"_"_s + pyTypePrefix + u"_Type"_s; s << typePtr << " = Shiboken::ObjectType::introduceWrapperType(\n" << indent; // 1:enclosingObject @@ -5861,7 +5912,7 @@ void CppGenerator::writeClassRegister(TextStream &s, s << "\",\n"; // 4:typeSpec - s << '&' << chopType(pyTypeName) << "_spec,\n"; + s << '&' << pyTypePrefix << "_spec,\n"; // 5:cppObjDtor QString dtorClassName = destructorClassName(metaClass, classContext); @@ -5887,7 +5938,7 @@ void CppGenerator::writeClassRegister(TextStream &s, else s << wrapperFlags.join(" | "); - s << outdent << ");\nauto *pyType = " << pyTypeName << "; // references " + s << outdent << ");\nauto *pyType = " << typePtr << "; // references " << typePtr << "\n" << outdent << "#if PYSIDE6_COMOPT_COMPRESS == 0\n" << indent << "InitSignatureStrings(pyType, " << initFunctionName << "_SignatureStrings);\n" @@ -5898,7 +5949,7 @@ void CppGenerator::writeClassRegister(TextStream &s, if (usePySideExtensions() && !classContext.forSmartPointer()) s << "SbkObjectType_SetPropertyStrings(pyType, " - << chopType(pyTypeName) << "_PropertyStrings);\n"; + << pyTypePrefix << "_PropertyStrings);\n"; s << globalTypeVarExpr << " = pyType;\n\n"; // Register conversions for the type. @@ -5944,7 +5995,7 @@ void CppGenerator::writeClassRegister(TextStream &s, if (!classContext.forSmartPointer() && !classEnums.isEmpty()) s << "// Pass the ..._EnumFlagInfo to the class.\n" - << "SbkObjectType_SetEnumFlagInfo(pyType, " << chopType(pyTypeName) + << "SbkObjectType_SetEnumFlagInfo(pyType, " << pyTypePrefix << "_EnumFlagInfo);\n\n"; writeEnumsInitialization(s, classEnums); @@ -5987,8 +6038,8 @@ void CppGenerator::writeStaticFieldInitialization(TextStream &s, if (parts.size() < 4) { s << "\nPyTypeObject *" << getSimpleClassStaticFieldsInitFunctionName(metaClass) << "(PyObject *module)\n{\n" << indent - << "auto *obType = PyObject_GetAttrString(module, \"" << metaClass->name() << "\");\n" - << "auto *type = reinterpret_cast(obType);\n" + << "Shiboken::AutoDecRef obType(PyObject_GetAttrString(module, \"" << metaClass->name() << "\"));\n" + << "auto *type = reinterpret_cast(obType.object());\n" << "Shiboken::AutoDecRef dict(PepType_GetDict(type));\n"; } else { s << "\nPyTypeObject *" << getSimpleClassStaticFieldsInitFunctionName(metaClass) @@ -6364,12 +6415,11 @@ void CppGenerator::writeInitFuncCall(TextStream &callStr, : "module"_L1; callStr << functionName << '(' << enclosing << ");\n"; } else if (hasParent) { - const QString &enclosingName = enclosingEntry->name(); - const auto parts = QStringView{enclosingName}.split(u"::", Qt::SkipEmptyParts); - const QString namePathPrefix = enclosingEntry->name().replace("::"_L1, "."_L1); + const QString &enclosingName = enclosingEntry->targetLangName(); + const auto parts = QStringView{enclosingName}.split(u".", Qt::SkipEmptyParts); callStr << "Shiboken::Module::AddTypeCreationFunction(" << "module, \"" << parts[0] << "\", " - << functionName << ", \"" << namePathPrefix << '.' << pythonName << "\");\n"; + << functionName << ", \"" << enclosingName << '.' << pythonName << "\");\n"; } else { callStr << "Shiboken::Module::AddTypeCreationFunction(" << "module, \"" << pythonName << "\", " @@ -6385,11 +6435,42 @@ static void writeSubModuleHandling(TextStream &s, const QString &moduleName, << subModuleOf << "\"));\n" << "if (parentModule.isNull())\n" << indent << "return nullptr;\n" << outdent - << "if (PyModule_AddObject(parentModule.object(), \"" << moduleName + << "if (PepModule_Add(parentModule.object(), \"" << moduleName << "\", module) < 0)\n" << indent << "return nullptr;\n" << outdent << outdent << "}\n"; } +static QString writeModuleDef(TextStream &s, const QString &moduleName, + const QString &execFunc) +{ + QString moduleDef = moduleName + "ModuleDef"_L1; + s << "static PyModuleDef_Slot " << moduleName << R"(ModuleSlots[] = { + {Py_mod_exec, reinterpret_cast()" << execFunc << R"()}, +#if !defined(PYPY_VERSION) && ((!defined(Py_LIMITED_API) && PY_VERSION_HEX >= 0x030C0000) || (defined(Py_LIMITED_API) && Py_LIMITED_API >= 0x030C0000)) + {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED}, +#endif +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_USED}, +#endif + {0, nullptr} +}; + +static struct PyModuleDef )" << moduleDef << R"( = { + /* m_base */ PyModuleDef_HEAD_INIT, + /* m_name */ ")" << moduleName << R"(", + /* m_doc */ nullptr, + /* m_size */ 0, + /* m_methods */ )" << moduleName << R"(Methods, + /* m_slots */ )" << moduleName << R"(ModuleSlots, + /* m_traverse */ nullptr, + /* m_clear */ nullptr, + /* m_free */ nullptr +}; + +)"; + return moduleDef; +} + bool CppGenerator::finishGeneration() { //Generate CPython wrapper file @@ -6489,8 +6570,9 @@ bool CppGenerator::finishGeneration() // write license comment s << licenseComment() << R"( -#include +#include #include +#include #include #include )"; @@ -6544,8 +6626,6 @@ bool CppGenerator::finishGeneration() << "Shiboken::Module::TypeInitStruct *" << cppApiVariableName() << " = nullptr;\n" << "// Backwards compatible structure with identical indexing.\n" << "PyTypeObject **" << cppApiVariableNameOld() << " = nullptr;\n" - << "// Current module's PyObject pointer.\n" - << "PyObject *" << pythonModuleObjectName() << " = nullptr;\n" << "// Current module's converter array.\n" << "SbkConverter **" << convertersVariableName() << " = nullptr;\n\n"; @@ -6572,7 +6652,7 @@ bool CppGenerator::finishGeneration() s << "// Global functions " << "------------------------------------------------------------\n" << s_globalFunctionImpl.toString() << '\n' - << "static PyMethodDef " << moduleName() << "_methods[] = {\n" << indent + << "static PyMethodDef " << moduleName() << "Methods[] = {\n" << indent << s_globalFunctionDef.toString() << METHOD_DEF_SENTINEL << outdent << "};\n\n" << "// Classes initialization functions " @@ -6632,7 +6712,7 @@ bool CppGenerator::finishGeneration() s << '\n'; } - QHash opaqueContainers; + OpaqueContainerTypeHash opaqueContainers; const auto &containers = api().instantiatedContainers(); QSet valueConverters; if (!containers.isEmpty()) { @@ -6650,100 +6730,95 @@ bool CppGenerator::finishGeneration() s << '\n'; } - s << "static struct PyModuleDef moduledef = {\n" - << " /* m_base */ PyModuleDef_HEAD_INIT,\n" - << " /* m_name */ \"" << moduleName() << "\",\n" - << " /* m_doc */ nullptr,\n" - << " /* m_size */ -1,\n" - << " /* m_methods */ " << moduleName() << "_methods,\n" - << " /* m_reload */ nullptr,\n" - << " /* m_traverse */ nullptr,\n" - << " /* m_clear */ nullptr,\n" - << " /* m_free */ nullptr\n};\n\n"; + const QString &modName = moduleName(); // PYSIDE-510: Create a signatures string for the introspection feature. writeSignatureStrings(s, signatureStream.toString(), moduleName(), "global functions"); writeInitInheritance(s); - // Write module init function - const QString globalModuleVar = pythonModuleObjectName(); - s << "extern \"C\" LIBSHIBOKEN_EXPORT PyObject *PyInit_" - << moduleName() << "()\n{\n" << indent; - // Guard against repeated invocation - s << "if (" << globalModuleVar << " != nullptr)\n" - << indent << "return " << globalModuleVar << ";\n" << outdent; + const QString convInitFunc = "initConverters_"_L1 + modName; + writeConverterInitFunc(s, convInitFunc, typeConversions, extendedConverters); + const QString containerConvInitFunc = "initContainerConverters_"_L1 + modName; + writeContainerConverterInitFunc(s, containerConvInitFunc, opaqueContainers); + QString opaqueContainerRegisterFunc; + if (!opaqueContainers.isEmpty()) { + opaqueContainerRegisterFunc = "registerOpaqueContainers_"_L1 + modName; + writeOpaqueContainerConverterRegisterFunc(s, opaqueContainerRegisterFunc, + opaqueContainers); + } + QString enumRegisterFunc; + QString qtEnumRegisterMetaTypeFunc; + if (!globalEnums.isEmpty()) { + enumRegisterFunc = "registerEnums_"_L1 + modName; + writeEnumsInitFunc(s, enumRegisterFunc, globalEnums); + if (usePySideExtensions()) { + qtEnumRegisterMetaTypeFunc = "registerEnumMetaTypes_"_L1 + modName; + writeQtEnumRegisterMetaTypeFunction(s, qtEnumRegisterMetaTypeFunc, globalEnums); + } + } - // module inject-code target/beginning - writeModuleCodeSnips(s, snips, TypeSystem::CodeSnipPositionBeginning, - TypeSystem::TargetLangCode); + const QString execFunc = "exec_"_L1 + modName; + writeModuleExecFunction(s, execFunc, opaqueContainerRegisterFunc, enumRegisterFunc, + s_classPythonDefines.toString(), classesWithStaticFields); - for (const QString &requiredModule : requiredModules) { - s << "{\n" << indent - << "Shiboken::AutoDecRef requiredModule(Shiboken::Module::import(\"" << requiredModule << "\"));\n" - << "if (requiredModule.isNull())\n" << indent - << "return nullptr;\n" << outdent - << cppApiVariableName(requiredModule) - << " = Shiboken::Module::getTypes(requiredModule);\n" - << convertersVariableName(requiredModule) - << " = Shiboken::Module::getTypeConverters(requiredModule);\n" << outdent - << "}\n\n"; - } - - int maxTypeIndex = getMaxTypeIndex() + api().instantiatedSmartPointers().size(); - if (maxTypeIndex) { - s << "// Create an array of wrapper types/names for the current module.\n" - << "static Shiboken::Module::TypeInitStruct cppApi[] = {\n" << indent; - - // Windows did not like an array of QString. - QStringList typeNames; - for (int idx = 0; idx < maxTypeIndex; ++idx) - typeNames.append("+++ unknown entry #"_L1 + QString::number(idx) - + " in "_L1 + moduleName()); - - collectFullTypeNamesArray(typeNames); - - for (const auto &typeName : typeNames) - s << "{nullptr, \"" << typeName << "\"},\n"; - - s << "{nullptr, nullptr}\n" << outdent << "};\n" - << "// The new global structure consisting of (type, name) pairs.\n" - << cppApiVariableName() << " = cppApi;\n"; - if (usePySideExtensions()) - s << "QT_WARNING_PUSH\nQT_WARNING_DISABLE_DEPRECATED\n"; - s << "// The backward compatible alias with upper case indexes.\n" - << cppApiVariableNameOld() << " = reinterpret_cast(cppApi);\n"; - if (usePySideExtensions()) - s << "QT_WARNING_POP\n"; - s << '\n'; - } + const QString moduleDef = writeModuleDef(s, modName, execFunc); - s << "// Create an array of primitive type converters for the current module.\n" - << "static SbkConverter *sbkConverters[SBK_" << moduleName() - << "_CONVERTERS_IDX_COUNT" << "];\n" - << convertersVariableName() << " = sbkConverters;\n\n" - << "PyObject *module = Shiboken::Module::create(\"" << moduleName() - << "\", &moduledef);\n\n" - << "// Make module available from global scope\n" - << globalModuleVar << " = module;\n\n"; + writeModuleInitFunction(s, moduleDef, execFunc, convInitFunc, containerConvInitFunc, qtEnumRegisterMetaTypeFunc); - const QString subModuleOf = typeDb->defaultTypeSystemType()->subModuleOf(); - if (!subModuleOf.isEmpty()) - writeSubModuleHandling(s, moduleName(), subModuleOf); + file.done(); + return true; +} - s << "// Initialize classes in the type system\n" - << s_classPythonDefines.toString(); +void CppGenerator::writeConverterInitFunc(TextStream &s, + const QString &funcName, + const QList &typeConversions, + const ExtendedConverterData &extendedConverters) +{ + s << "static void " << funcName << "()\n{\n" << indent; if (!typeConversions.isEmpty()) { - s << '\n'; + s << "// Type conversions.\n"; for (const auto &conversion : typeConversions) { writePrimitiveConverterInitialization(s, conversion); s << '\n'; } } - if (!containers.isEmpty()) { + if (!extendedConverters.isEmpty()) { s << '\n'; + for (auto it = extendedConverters.cbegin(), end = extendedConverters.cend(); it != end; ++it) { + writeExtendedConverterInitialization(s, it.key(), it.value()); + s << '\n'; + } + } + + const PrimitiveTypeEntryCList &primitiveTypeList = primitiveTypes(); + if (!primitiveTypeList.isEmpty()) { + s << "// Register primitive types converters.\n"; + for (const auto &pte : primitiveTypeList) { + if (!pte->generateCode() || !isCppPrimitive(pte)) + continue; + if (!pte->referencesType()) + continue; + TypeEntryCPtr referencedType = basicReferencedTypeEntry(pte); + s << registerConverterName(pte->qualifiedCppName(), converterObject(referencedType), + registerConverterName::Alias + | registerConverterName::PartiallyQualifiedAliases) << '\n'; + } + } + + s << outdent << "}\n\n"; +} + +void CppGenerator::writeContainerConverterInitFunc(TextStream &s, + const QString &funcName, + const OpaqueContainerTypeHash &opaqueContainers) const +{ + s << "static void " << funcName << "()\n{\n" << indent; + + const auto &containers = api().instantiatedContainers(); + if (!containers.isEmpty()) { for (const AbstractMetaType &container : containers) { const QString converterObj = writeContainerConverterInitialization(s, container, api()); const auto it = opaqueContainers.constFind(container); @@ -6756,49 +6831,157 @@ bool CppGenerator::finishGeneration() } } - if (!opaqueContainers.isEmpty()) { - s << "\n// Opaque container type registration\n" - << "PyObject *ob_type{};\n"; - if (usePySideExtensions()) { - const bool hasQVariantConversion = - std::any_of(opaqueContainers.cbegin(), opaqueContainers.cend(), - [](const OpaqueContainerData &d) { return d.hasQVariantConversion; }); - if (hasQVariantConversion) { - const char qVariantConverterVar[] = "qVariantConverter"; - s << "auto *" << qVariantConverterVar - << " = Shiboken::Conversions::getConverter(\"QVariant\");\n" - << "Q_ASSERT(" << qVariantConverterVar << " != nullptr);\n"; - } + s << outdent << "}\n\n"; +} + +void CppGenerator::writeOpaqueContainerConverterRegisterFunc(TextStream &s, const QString &funcName, + const OpaqueContainerTypeHash &opaqueContainers) +{ + s << "static void " << funcName << "(PyObject *module)\n{\n" << indent + << "PyTypeObject *pyType{};\n"; + if (usePySideExtensions()) { + const bool hasQVariantConversion = + std::any_of(opaqueContainers.cbegin(), opaqueContainers.cend(), + [](const OpaqueContainerData &d) { return d.hasQVariantConversion; }); + if (hasQVariantConversion) { + const char qVariantConverterVar[] = "qVariantConverter"; + s << "auto *" << qVariantConverterVar + << " = Shiboken::Conversions::getConverter(\"QVariant\");\n" + << "Q_ASSERT(" << qVariantConverterVar << " != nullptr);\n"; } - for (const auto &d : opaqueContainers) - s << d.registrationCode; - s << '\n'; } + for (const auto &d : opaqueContainers) + s << d.registrationCode; + s << outdent << "}\n\n"; +} - if (!extendedConverters.isEmpty()) { - s << '\n'; - for (ExtendedConverterData::const_iterator it = extendedConverters.cbegin(), end = extendedConverters.cend(); it != end; ++it) { - writeExtendedConverterInitialization(s, it.key(), it.value()); - s << '\n'; +void CppGenerator::writeModuleInitFunction(TextStream &s, const QString &moduleDef, + const QString &execFunc, const QString &convInitFunc, + const QString &containerConvInitFunc, + const QString &qtEnumRegisterMetaTypeFunc) +{ + s << "extern \"C\" LIBSHIBOKEN_EXPORT PyObject *PyInit_" + << moduleName() << "()\n{\n" << indent + << "Shiboken::init();\n\n"; + + // Static initialization: Create converter/type arrays and retrieve arrays + // of the required modules for initializing the converters. + const int maxTypeIndex = getMaxTypeIndex() + api().instantiatedSmartPointers().size(); + if (maxTypeIndex) { + s << "// Create an array of wrapper types/names for the current module.\n" + << "static Shiboken::Module::TypeInitStruct cppApi[] = {\n" << indent; + + // Windows did not like an array of QString. + QStringList typeNames; + for (int idx = 0; idx < maxTypeIndex; ++idx) + typeNames.append("+++ unknown entry #"_L1 + QString::number(idx) + + " in "_L1 + moduleName()); + + collectFullTypeNamesArray(typeNames); + + for (const auto &typeName : typeNames) + s << "{nullptr, \"" << typeName << "\"},\n"; + + s << "{nullptr, nullptr}\n" << outdent << "};\n" + << "// The new global structure consisting of (type, name) pairs.\n" + << cppApiVariableName() << " = cppApi;\n"; + if (usePySideExtensions()) + s << "QT_WARNING_PUSH\nQT_WARNING_DISABLE_DEPRECATED\n"; + s << "// The backward compatible alias with upper case indexes.\n" + << cppApiVariableNameOld() << " = reinterpret_cast(cppApi);\n"; + if (usePySideExtensions()) + s << "QT_WARNING_POP\n"; + s << '\n'; + } + + s << "// Create an array of primitive type converters for the current module.\n" + << "static SbkConverter *sbkConverters[SBK_" << moduleName() + << "_CONVERTERS_IDX_COUNT" << "];\n" + << convertersVariableName() << " = sbkConverters;\n\n"; + + const TypeDatabase *typeDb = TypeDatabase::instance(); + const CodeSnipList snips = typeDb->defaultTypeSystemType()->codeSnips(); + + writeModuleCodeSnips(s, snips, TypeSystem::CodeSnipPositionBeginning, + TypeSystem::TargetLangCode); + + const QStringList &requiredModules = typeDb->requiredTargetImports(); + for (const QString &requiredModule : requiredModules) { + s << "{\n" << indent + << "Shiboken::AutoDecRef requiredModule(Shiboken::Module::import(\"" << requiredModule << "\"));\n" + << "if (requiredModule.isNull())\n" << indent + << "return nullptr;\n" << outdent + << cppApiVariableName(requiredModule) + << " = Shiboken::Module::getTypes(requiredModule);\n" + << convertersVariableName(requiredModule) + << " = Shiboken::Module::getTypeConverters(requiredModule);\n" << outdent + << "}\n\n"; + } + + s << convInitFunc << "();\n" << containerConvInitFunc << "();\n"; + if (!qtEnumRegisterMetaTypeFunc.isEmpty()) + s << qtEnumRegisterMetaTypeFunc << "();\n"; + s << '\n'; + + // As of 8/25, Nuitka does not support multi-phase initialization. Fall back + s << "PyObject *module = nullptr;\n" + << "if (Shiboken::isCompiled()) {\n" << indent + << moduleDef << ".m_size = -1;\n" + << moduleDef << ".m_slots = nullptr;\n" + << "module = Shiboken::Module::createOnly(\"" << moduleName() + << "\", &" << moduleDef << ");\n" + << "if (module == nullptr)\n" << indent << "return nullptr;\n" << outdent + << "#ifdef Py_GIL_DISABLED\n" + << "PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED);\n" + << "#endif\n" + << "if (" << execFunc << "(module) != 0)\n" << indent << "return nullptr;\n" << outdent + << outdent << "} else {\n" << indent; + // Multi-phase initialization (exec() will be called by CPython). + s << "module = PyModuleDef_Init(&" << moduleDef << ");\n" << outdent << "}\n" + << "return module;\n" << outdent << "}\n\n"; +} + +void CppGenerator::writeQtEnumRegisterMetaTypeFunction(TextStream &s, + const QString &name, + const AbstractMetaEnumList &globalEnums) +{ + s << "static void " << name << "()\n{\n" << indent; + for (const AbstractMetaEnum &metaEnum : globalEnums) { + if (!metaEnum.isAnonymous()) { + ConfigurableScope configScope(s, metaEnum.typeEntry()); + s << "qRegisterMetaType< " << Generator::getFullTypeName(metaEnum.typeEntry()) + << " >(\"" << metaEnum.name() << "\");\n"; } } + s << outdent << "}\n\n"; +} - writeEnumsInitialization(s, globalEnums); +void CppGenerator::writeModuleExecFunction(TextStream &s, const QString &name, + const QString &opaqueContainerRegisterFunc, + const QString &enumRegisterFunc, + const QString &classPythonDefines, + const AbstractMetaClassCList &classesWithStaticFields) +{ + // Code to run in an module instance of a subinterpreter (Py_mod_exec) + s << "extern \"C\" {\nstatic int " << name << "(PyObject *module)\n{\n" << indent + << "Shiboken::Module::exec(module);\n\n"; - s << "// Register primitive types converters.\n"; - const PrimitiveTypeEntryCList &primitiveTypeList = primitiveTypes(); - for (const auto &pte : primitiveTypeList) { - if (!pte->generateCode() || !isCppPrimitive(pte)) - continue; - if (!pte->referencesType()) - continue; - TypeEntryCPtr referencedType = basicReferencedTypeEntry(pte); - s << registerConverterName(pte->qualifiedCppName(), converterObject(referencedType), - registerConverterName::Alias - | registerConverterName::PartiallyQualifiedAliases); - } + // module inject-code target/beginning + const TypeDatabase *typeDb = TypeDatabase::instance(); + const CodeSnipList snips = typeDb->defaultTypeSystemType()->codeSnips(); + + const QString subModuleOf = typeDb->defaultTypeSystemType()->subModuleOf(); + if (!subModuleOf.isEmpty()) + writeSubModuleHandling(s, moduleName(), subModuleOf); + s << "// Initialize classes in the type system\n" << classPythonDefines << '\n'; + if (!opaqueContainerRegisterFunc.isEmpty()) + s << opaqueContainerRegisterFunc << "(module);\n"; + if (!enumRegisterFunc.isEmpty()) + s << enumRegisterFunc << "(module);\n"; s << '\n'; + + const int maxTypeIndex = getMaxTypeIndex() + api().instantiatedSmartPointers().size(); if (maxTypeIndex) s << "Shiboken::Module::registerTypes(module, " << cppApiVariableName() << ");\n"; s << "Shiboken::Module::registerTypeConverters(module, " << convertersVariableName() << ");\n"; @@ -6816,7 +6999,7 @@ bool CppGenerator::finishGeneration() s << '\n' << initInheritanceFunction << "();\n" << "\nif (" << shibokenErrorsOccurred << ") {\n" << indent << "PyErr_Print();\n" - << "Py_FatalError(\"can't initialize module " << moduleName() << "\");\n" + << "Py_FatalError(\"shiboken: can't initialize module " << moduleName() << "\");\n" << outdent << "}\n"; // module inject-code target/end @@ -6825,29 +7008,17 @@ bool CppGenerator::finishGeneration() // module inject-code native/end writeModuleCodeSnips(s, snips, TypeSystem::CodeSnipPositionEnd, TypeSystem::NativeCode); - if (usePySideExtensions()) { - for (const AbstractMetaEnum &metaEnum : std::as_const(globalEnums)) - if (!metaEnum.isAnonymous()) { - ConfigurableScope configScope(s, metaEnum.typeEntry()); - s << "qRegisterMetaType< " << getFullTypeName(metaEnum.typeEntry()) - << " >(\"" << metaEnum.name() << "\");\n"; - } - - // cleanup staticMetaObject attribute + if (usePySideExtensions()) // cleanup staticMetaObject attribute s << "PySide::registerCleanupFunction(cleanTypesAttributes);\n\n"; - } // finish the rest of get_signature() initialization. s << outdent << "#if PYSIDE6_COMOPT_COMPRESS == 0\n" << indent << "FinishSignatureInitialization(module, " << moduleName() << "_SignatureStrings);\n" << outdent << "#else\n" << indent << "if (FinishSignatureInitBytes(module, " << moduleName() << "_SignatureBytes, " - << moduleName() << "_SignatureByteSize) < 0)\n" << indent << "return {};\n" << outdent + << moduleName() << "_SignatureByteSize) < 0)\n" << indent << "return -1;\n" << outdent << outdent << "#endif\n" << indent - << "\nreturn module;\n" << outdent << "}\n"; - - file.done(); - return true; + << "\nreturn 0;\n" << outdent << "}\n} // extern \"C\"\n\n"; } static ArgumentOwner getArgumentOwner(const AbstractMetaFunctionCPtr &func, int argIndex) diff --git a/sources/shiboken6/generator/shiboken/cppgenerator.h b/sources/shiboken6/generator/shiboken/cppgenerator.h index 0ae86dd3..2510fa98 100644 --- a/sources/shiboken6/generator/shiboken/cppgenerator.h +++ b/sources/shiboken6/generator/shiboken/cppgenerator.h @@ -114,6 +114,9 @@ private: static void writeCustomConverterRegister(TextStream &s, const CustomConversionPtr &customConversion, const QString &converterVar); + static void writeTemplateCustomConverterRegister(TextStream &s, + const AbstractMetaType &type, + QString converter = {}); void writeContainerConverterFunctions(TextStream &s, const AbstractMetaType &containerType) const; @@ -127,6 +130,7 @@ private: QString registrationCode; bool hasQVariantConversion = false; }; + using OpaqueContainerTypeHash = QHash; OpaqueContainerData writeOpaqueContainerConverterFunctions(TextStream &s, @@ -331,7 +335,7 @@ private: /// Writes a C++ to Python conversion function. void writeCppToPythonFunction(TextStream &s, const QString &code, const QString &sourceTypeName, - const QString &targetTypeName = {}) const; + const QString &targetTypeName = {}, bool withType = false) const; void writeCppToPythonFunction(TextStream &s, const CustomConversionPtr &customConversion) const; void writeCppToPythonFunction(TextStream &s, const AbstractMetaType &containerType) const; /// Main target type name of a container (for naming the functions). @@ -361,12 +365,13 @@ private: const TargetToNativeConversion &toNative, const TypeEntryCPtr &targetType) const; - /// Writes a pair of Python to C++ conversion and check functions for instantiated container types. + /// Writes a pair of Python to C++ conversion and check functions for instantiated + /// template (smart pointer/container types). void writePythonToCppConversionFunctions(TextStream &s, - const AbstractMetaType &containerType) const; + const AbstractMetaType &templateType) const; void writePythonToCppConversionFunction(TextStream &s, - const AbstractMetaType &containerType, + const AbstractMetaType &templateType, const TargetToNativeConversion &conv) const; static void writeAddPythonToCppConversion(TextStream &s, const QString &converterVar, @@ -419,6 +424,8 @@ private: void writeClassDefinition(TextStream &s, const AbstractMetaClassCPtr &metaClass, const GeneratorContext &classContext); + static void writeClassTypeFunction(TextStream &s, + const AbstractMetaClassCPtr &metaClass); QByteArrayList methodDefinitionParameters(const OverloadData &overloadData) const; QList methodDefinitionEntries(const OverloadData &overloadData) const; @@ -474,7 +481,9 @@ private: void writeRichCompareFunction(TextStream &s, TextStream &t, const GeneratorContext &context) const; void writeSmartPointerRichCompareFunction(TextStream &s, const GeneratorContext &context) const; - static void writeEnumsInitialization(TextStream &s, AbstractMetaEnumList &enums); + static void writeEnumsInitialization(TextStream &s, const AbstractMetaEnumList &enums); + static void writeEnumsInitFunc(TextStream &s, const QString &funcName, + const AbstractMetaEnumList &enums); static bool writeEnumInitialization(TextStream &s, const AbstractMetaEnum &metaEnum); static void writeSignalInitialization(TextStream &s, const AbstractMetaClassCPtr &metaClass); @@ -501,6 +510,25 @@ private: const TypeEntryCPtr &externalType, const AbstractMetaClassCList &conversions); + void writeModuleInitFunction(TextStream &s, const QString &moduleDef, + const QString &execFunc, const QString &convInitFunc, + const QString &containerConvInitFunc, + const QString &qtEnumRegisterMetaTypeFunc); + void writeModuleExecFunction(TextStream &s, const QString &name, + const QString &opaqueContainerRegisterFunc, + const QString &enumRegisterFunc, + const QString &classPythonDefines, + const AbstractMetaClassCList &classesWithStaticFields); + static void writeConverterInitFunc(TextStream &s, const QString &funcName, + const QList &typeConversions, + const ExtendedConverterData &extendedConverters); + void writeContainerConverterInitFunc(TextStream &s, const QString &funcName, + const OpaqueContainerTypeHash &opaqueContainers) const; + static void writeOpaqueContainerConverterRegisterFunc(TextStream &s, const QString &funcName, + const OpaqueContainerTypeHash &opaqueContainers); + static void writeQtEnumRegisterMetaTypeFunction(TextStream &s, const QString &name, + const AbstractMetaEnumList &globalEnums); + void writeParentChildManagement(TextStream &s, const AbstractMetaFunctionCPtr &func, bool usesPyArgs, bool userHeuristicForReturn) const; @@ -564,7 +592,6 @@ private: { return boolCast(metaClass).has_value(); } void clearTpFuncs(); - static QString chopType(QString s); static QString typeInitStructHelper(const TypeEntryCPtr &te, const QString &varName); diff --git a/sources/shiboken6/generator/shiboken/cppgenerator_container.cpp b/sources/shiboken6/generator/shiboken/cppgenerator_container.cpp index 8ff47315..826ad89a 100644 --- a/sources/shiboken6/generator/shiboken/cppgenerator_container.cpp +++ b/sources/shiboken6/generator/shiboken/cppgenerator_container.cpp @@ -285,9 +285,9 @@ CppGenerator::OpaqueContainerData result.pythonToConverterFunctionName); TextStream registrationStr(&result.registrationCode, TextStream::Language::Cpp); - registrationStr << "ob_type = reinterpret_cast(" - << typeFName << "());\nPy_XINCREF(ob_type);\nPyModule_AddObject(module, \"" - << result.name << "\", ob_type);\n"; + registrationStr << "pyType = " << typeFName << "();\n" + << "Py_XINCREF(reinterpret_cast(pyType));\n" + << "PepModule_AddType(module, pyType);\n"; if (!result.hasQVariantConversion) return result; diff --git a/sources/shiboken6/generator/shiboken/cppgenerator_smartpointer.cpp b/sources/shiboken6/generator/shiboken/cppgenerator_smartpointer.cpp index fec67659..c87888c2 100644 --- a/sources/shiboken6/generator/shiboken/cppgenerator_smartpointer.cpp +++ b/sources/shiboken6/generator/shiboken/cppgenerator_smartpointer.cpp @@ -182,7 +182,7 @@ void CppGenerator::generateSmartPointerClass(TextStream &s, const QString &methodsDefinitions = md.toString(); const QString &singleMethodDefinitions = smd.toString(); - const QString className = chopType(cpythonTypeName(typeEntry)); + const QString className = cpythonBaseName(typeEntry); // Write single method definitions s << singleMethodDefinitions; @@ -210,6 +210,7 @@ void CppGenerator::generateSmartPointerClass(TextStream &s, writeTpTraverseFunction(s, metaClass); writeTpClearFunction(s, metaClass); + writeClassTypeFunction(s, metaClass); writeClassDefinition(s, metaClass, classContext); s << '\n'; @@ -231,13 +232,16 @@ void CppGenerator::generateSmartPointerClass(TextStream &s, void CppGenerator::writeSmartPointerConverterFunctions(TextStream &s, const AbstractMetaType &smartPointerType) const { + auto smartPointerTypeEntry = + std::static_pointer_cast(smartPointerType.typeEntry()); + + if (smartPointerTypeEntry->hasCustomConversion()) + writePythonToCppConversionFunctions(s, smartPointerType); + const auto baseClasses = findSmartPointeeBaseClasses(api(), smartPointerType); if (baseClasses.isEmpty()) return; - auto smartPointerTypeEntry = - std::static_pointer_cast(smartPointerType.typeEntry()); - // TODO: Missing conversion to smart pointer pointer type: s << "// Register smartpointer conversion for all derived classes\n"; @@ -289,6 +293,8 @@ void CppGenerator::writeSmartPointerConverterInitialization(TextStream &s, writeAddPythonToCppConversion(s, targetConverter, toCpp, isConv); }; + writeTemplateCustomConverterRegister(s, type); + const auto classes = findSmartPointeeBaseClasses(api(), type); if (classes.isEmpty()) return; diff --git a/sources/shiboken6/generator/shiboken/generatorstrings.h b/sources/shiboken6/generator/shiboken/generatorstrings.h index 011a4a3e..fb0df17f 100644 --- a/sources/shiboken6/generator/shiboken/generatorstrings.h +++ b/sources/shiboken6/generator/shiboken/generatorstrings.h @@ -33,6 +33,7 @@ constexpr auto CPP_ARG0 = QLatin1StringView("cppArg0"); extern const char *const METHOD_DEF_SENTINEL; extern const char *const PYTHON_TO_CPPCONVERSION_STRUCT; extern const char *const openTargetExternC; +extern const char *const openExternC; extern const char *const closeExternC; extern const char *const richCompareComment; diff --git a/sources/shiboken6/generator/shiboken/headergenerator.cpp b/sources/shiboken6/generator/shiboken/headergenerator.cpp index b6491afe..6ffeef9a 100644 --- a/sources/shiboken6/generator/shiboken/headergenerator.cpp +++ b/sources/shiboken6/generator/shiboken/headergenerator.cpp @@ -185,6 +185,7 @@ void HeaderGenerator::writeWrapperClass(TextStream &s, for( const auto &includeGroup : includeGroups) s << includeGroup; } + s << "#include \n\n#include \n"; s << "namespace Shiboken { struct AutoDecRef; class GilState; }\n\n"; @@ -240,6 +241,10 @@ void HeaderGenerator::writeWrapperClassDeclaration(TextStream &s, << " : public " << metaClass->qualifiedCppName() << "\n{\npublic:\n" << indent; + writeClassCodeSnips(s, metaClass->typeEntry()->codeSnips(), + TypeSystem::CodeSnipPositionWrapperDeclaration, + TypeSystem::NativeCode, classContext); + writeProtectedEnums(s, classContext); writeSpecialFunctions(s, wrapperName, classContext); @@ -284,9 +289,10 @@ void *qt_metacast(const char *_clname) override; } if (needsMethodCache) { - s << "mutable bool m_PyMethodCache[" << maxOverrides << "] = {false"; + s << "mutable std::array m_PyMethodCache = {nullptr"; for (int i = 1; i < maxOverrides; ++i) - s << ", false"; + s << ", nullptr"; s << "};\n"; } @@ -722,17 +728,25 @@ HeaderGenerator::IndexValues HeaderGenerator::collectConverterIndexes() const } // PYSIDE-2404: Write the enums in unchanged case for reuse in type imports. -// For conpatibility, we create them in uppercase, too and with +// For compatibility, we create them in uppercase, too and with // doubled index for emulating the former type-only case. // // FIXME: Remove in PySide 7. (See the note in `parser.py`) -// -static IndexValue typeIndexUpper(struct IndexValue const &ti) + +static IndexValue indexUpper(IndexValue ti) // converter indexes (old macro compatibility) { QString modi = ti.name.toUpper(); if (modi == ti.name) - modi = u"// "_s + modi; - return {modi, ti.value * 2, ti.comment}; + modi.prepend("// "_L1); + ti.name = modi; + return ti; +} + +static IndexValue typeIndexUpper(const IndexValue &ti) // type indexes (PYSIDE-2404) +{ + IndexValue result = indexUpper(ti); + result.value *= 2; + return result; } bool HeaderGenerator::finishGeneration() @@ -782,7 +796,7 @@ bool HeaderGenerator::finishGeneration() const auto converterIndexes = collectConverterIndexes(); macrosStream << "// Converter indices\nenum [[deprecated]] : int {\n"; for (const auto &ci : converterIndexes) - macrosStream << typeIndexUpper(ci); + macrosStream << indexUpper(ci); macrosStream << "};\n\n"; macrosStream << "// Converter indices\nenum : int {\n"; @@ -800,7 +814,7 @@ bool HeaderGenerator::finishGeneration() TextStream privateTypeFunctions(&privateParameters.typeFunctions, TextStream::Language::Cpp); for (const AbstractMetaEnum &cppEnum : api().globalEnums()) { - if (!cppEnum.isAnonymous()) { + if (!cppEnum.isAnonymous() && cppEnum.typeEntry()->aliasMode() != EnumTypeEntry::AliasSource) { const auto te = cppEnum.typeEntry(); if (te->hasConfigCondition()) parameters.conditionalIncludes[te->configCondition()].append(te->include()); @@ -832,8 +846,10 @@ bool HeaderGenerator::finishGeneration() ConfigurableScope configScope(typeFunctionsStr, classType); for (const AbstractMetaEnum &cppEnum : metaClass->enums()) { - if (cppEnum.isAnonymous() || cppEnum.isPrivate()) + if (cppEnum.isAnonymous() || cppEnum.isPrivate() + || cppEnum.typeEntry()->aliasMode() == EnumTypeEntry::AliasSource) { continue; + } if (const auto inc = cppEnum.typeEntry()->include(); inc != classInclude) par.includes.insert(inc); writeProtectedEnumSurrogate(protEnumsSurrogates, cppEnum); diff --git a/sources/shiboken6/generator/shiboken/shibokengenerator.cpp b/sources/shiboken6/generator/shiboken/shibokengenerator.cpp index def95e3f..c4f0d4ca 100644 --- a/sources/shiboken6/generator/shiboken/shibokengenerator.cpp +++ b/sources/shiboken6/generator/shiboken/shibokengenerator.cpp @@ -88,6 +88,7 @@ const char *const openTargetExternC = R"( extern "C" { )"; +const char *const openExternC = "extern \"C\" {\n"; const char *const closeExternC = "} // extern \"C\"\n\n"; const char *const richCompareComment = "// PYSIDE-74: By default, we redirect to object's tp_richcompare (which is `==`, `!=`).\n"; @@ -633,13 +634,6 @@ bool ShibokenGenerator::shouldRejectNullPointerArgument(const AbstractMetaFuncti return false; } -QString ShibokenGenerator::cpythonBaseName(const AbstractMetaType &type) -{ - if (type.isCString()) - return u"PyString"_s; - return cpythonBaseName(type.typeEntry()); -} - QString ShibokenGenerator::cpythonBaseName(const AbstractMetaClassCPtr &metaClass) { return cpythonBaseName(metaClass->typeEntry()); @@ -663,25 +657,10 @@ QString ShibokenGenerator::containerCpythonBaseName(const ContainerTypeEntryCPtr return cPySequenceT; } -QString ShibokenGenerator::cpythonBaseName(const TypeEntryCPtr &type) -{ - QString baseName; - if (type->isWrapperType() || type->isNamespace()) { // && type->referenceType() == NoReference) { - baseName = u"Sbk_"_s + type->name(); - } else if (type->isPrimitive()) { - const auto ptype = basicReferencedTypeEntry(type); - baseName = ptype->hasTargetLangApiType() - ? ptype->targetLangApiName() : pythonPrimitiveTypeName(ptype->name()); - } else if (type->isEnum()) { - baseName = cpythonEnumName(std::static_pointer_cast(type)); - } else if (type->isFlags()) { - baseName = cpythonFlagsName(std::static_pointer_cast(type)); - } else if (type->isContainer()) { - const auto ctype = std::static_pointer_cast(type); - baseName = containerCpythonBaseName(ctype); - } else { - baseName = cPyObjectT; - } +QString ShibokenGenerator::cpythonBaseName(const ComplexTypeEntryCPtr &type) +{ + Q_ASSERT(type->isWrapperType() || type->isNamespace()); + QString baseName = u"Sbk_"_s + type->name(); return baseName.replace(u"::"_s, u"_"_s); } @@ -690,7 +669,7 @@ QString ShibokenGenerator::cpythonTypeName(const AbstractMetaClassCPtr &metaClas return cpythonTypeName(metaClass->typeEntry()); } -QString ShibokenGenerator::cpythonTypeName(const TypeEntryCPtr &type) +QString ShibokenGenerator::cpythonTypeName(const ComplexTypeEntryCPtr &type) { return cpythonBaseName(type) + u"_TypeF()"_s; } @@ -1021,8 +1000,9 @@ QString ShibokenGenerator::cpythonIsConvertibleFunction(const TypeEntryCPtr &typ result += u"("_s + cpythonTypeNameExt(type) + u", "_s; return result; } - return QString::fromLatin1("Shiboken::Conversions::isPythonToCppConvertible(%1, ") - .arg(converterObject(type)); + + return "Shiboken::Conversions::isPythonToCppConvertible("_L1 + + converterObject(type) + ", "_L1; } QString ShibokenGenerator::cpythonIsConvertibleFunction(const AbstractMetaType &metaType) @@ -1386,6 +1366,12 @@ void ShibokenGenerator::processClassCodeSnip(QString &code, const GeneratorConte processCodeSnip(code, context.effectiveClassName()); } +void ShibokenGenerator::processTypeCheckCodeSnip(QString &code, const QString &context) const +{ + code.replace("%in"_L1, "pyIn"_L1); + processCodeSnip(code, context); +} + void ShibokenGenerator::processCodeSnip(QString &code) const { // replace "toPython" converters @@ -2303,7 +2289,7 @@ static AbstractMetaFunctionCList filterFunctions(const OverloadRemovalRules &rem if (const auto index = types.indexOf(rule.type); index != -1) { for (const auto &redundantType : rule.redundantTypes) { if (const auto index2 = types.indexOf(redundantType); index2 != -1) { - auto redundant = overloads.at(index2); + const auto &redundant = overloads.at(index2); if (!result.contains(redundant)) { // nested long->int->short rule? ReportHandler::addGeneralMessage(msgRemoveRedundantOverload(redundant, rule.type)); result.append(redundant); @@ -2751,10 +2737,7 @@ QString ShibokenGenerator::pythonModuleObjectName(const QString &moduleName) QString ShibokenGenerator::convertersVariableName(const QString &moduleName) { - QString result = cppApiVariableNameOld(moduleName); - result.chop(1); - result.append(u"Converters"_s); - return result; + return "Sbk"_L1 + moduleCppPrefix(moduleName) + "TypeConverters"_L1; } static QString processInstantiationsVariableName(const AbstractMetaType &type) diff --git a/sources/shiboken6/generator/shiboken/shibokengenerator.h b/sources/shiboken6/generator/shiboken/shibokengenerator.h index a019e02b..67722b97 100644 --- a/sources/shiboken6/generator/shiboken/shibokengenerator.h +++ b/sources/shiboken6/generator/shiboken/shibokengenerator.h @@ -99,6 +99,10 @@ public: static QString minimalConstructorExpression(const ApiExtractorResult &api, const TypeEntryCPtr &type); + /// Return the name of the _TypeF() function generated to get the PyTypeObject + static QString cpythonTypeName(const AbstractMetaClassCPtr &metaClass); + static QString cpythonTypeName(const ComplexTypeEntryCPtr &type); + protected: bool doSetup() override; @@ -158,6 +162,8 @@ protected: void processCodeSnip(QString &code) const; void processCodeSnip(QString &code, const QString &context) const; void processClassCodeSnip(QString &code, const GeneratorContext &context) const; + /// Replaces variables in a custom conversion type check snippet + void processTypeCheckCodeSnip(QString &code, const QString &context) const; /** * Verifies if any of the function's code injections makes a call @@ -259,12 +265,10 @@ protected: static QString converterObject(const AbstractMetaType &type) ; static QString converterObject(const TypeEntryCPtr &type); + /// Return a name prefixed by Sbk_ which can be used for naming variables in the code static QString cpythonBaseName(const AbstractMetaClassCPtr &metaClass); - static QString cpythonBaseName(const TypeEntryCPtr &type); + static QString cpythonBaseName(const ComplexTypeEntryCPtr &type); static QString containerCpythonBaseName(const ContainerTypeEntryCPtr &ctype); - static QString cpythonBaseName(const AbstractMetaType &type); - static QString cpythonTypeName(const AbstractMetaClassCPtr &metaClass); - static QString cpythonTypeName(const TypeEntryCPtr &type); static QString cpythonTypeNameExtSet(const TypeEntryCPtr &type); static QString cpythonTypeNameExtSet(const AbstractMetaType &type); static QString cpythonTypeNameExt(const TypeEntryCPtr &type); diff --git a/sources/shiboken6/libshiboken/CMakeLists.txt b/sources/shiboken6/libshiboken/CMakeLists.txt index 9bf16519..85f94331 100644 --- a/sources/shiboken6/libshiboken/CMakeLists.txt +++ b/sources/shiboken6/libshiboken/CMakeLists.txt @@ -75,9 +75,11 @@ sbkcppstring.cpp sbkcppstring.h sbkcpptonumpy.h sbkenum.cpp sbkenum.h sbkerrors.cpp sbkerrors.h sbkfeature_base.cpp sbkfeature_base.h -sbkmodule.cpp sbkmodule.h +sbkmodule.cpp sbkmodule.h sbkmodule_p.h sbknumpy.cpp sbknumpycheck.h sbknumpyview.h +sbkpep.h +sbkpepbuffer.h sbkpython.h sbksmartpointer.cpp sbksmartpointer.h sbkstaticstrings.cpp sbkstaticstrings.h sbkstaticstrings_p.h @@ -109,7 +111,7 @@ add_library(Shiboken6::libshiboken ALIAS libshiboken) target_include_directories(libshiboken PUBLIC $ $ - $ + $ ) if (NOT "${NUMPY_INCLUDE_DIR}" STREQUAL "") @@ -170,6 +172,7 @@ install(FILES sbkerrors.h sbkfeature_base.h sbkmodule.h + sbkmodule_p.h sbknumpycheck.h sbknumpyview.h sbkstring.h @@ -181,6 +184,8 @@ install(FILES shibokenmacros.h threadstatesaver.h shibokenbuffer.h + sbkpep.h + sbkpepbuffer.h sbkpython.h sbkwindows.h pep384impl.h @@ -192,10 +197,27 @@ install(FILES signature.h signature_p.h - DESTINATION include/shiboken6${shiboken6_SUFFIX}) + DESTINATION shiboken6${shiboken6_SUFFIX}/include) install(TARGETS libshiboken EXPORT Shiboken6Targets LIBRARY DESTINATION "${LIB_INSTALL_DIR}" ARCHIVE DESTINATION "${LIB_INSTALL_DIR}" RUNTIME DESTINATION bin) install(EXPORT Shiboken6Targets NAMESPACE Shiboken6:: DESTINATION ${LIB_INSTALL_DIR}/cmake/Shiboken6) + +# wheel specific installation +if(NOT is_pyside6_superproject_build) + + set_target_properties(libshiboken PROPERTIES + VERSION ${libshiboken_SOVERSION}) + + install(TARGETS libshiboken EXPORT Shiboken6WheelTargets + LIBRARY DESTINATION "shiboken6" + ARCHIVE DESTINATION "shiboken6" + RUNTIME DESTINATION "shiboken6") + + install(EXPORT Shiboken6WheelTargets + NAMESPACE Shiboken6:: + DESTINATION "${LIB_INSTALL_DIR}/wheels/cmake/Shiboken6" + FILE Shiboken6Targets.cmake) +endif() diff --git a/sources/shiboken6/libshiboken/basewrapper.cpp b/sources/shiboken6/libshiboken/basewrapper.cpp index 4bae93b0..eb9d47c7 100644 --- a/sources/shiboken6/libshiboken/basewrapper.cpp +++ b/sources/shiboken6/libshiboken/basewrapper.cpp @@ -21,6 +21,7 @@ #include "voidptr.h" #include +#include #include #include #include @@ -92,7 +93,7 @@ DestructorEntries getDestructorEntries(SbkObject *o) { DestructorEntries result; void **cptrs = o->d->cptr; - walkThroughBases(Py_TYPE(o), [&result, cptrs](PyTypeObject *node) { + walkThroughBases(Shiboken::pyType(o), [&result, cptrs](PyTypeObject *node) { auto *sotp = PepType_SOTP(node); auto index = result.size(); result.push_back(DestructorEntry{sotp->cpp_dtor, @@ -415,12 +416,8 @@ static void SbkDeallocWrapperCommon(PyObject *pyObj, bool canDelete) } } - PyObject *error_type{}; - PyObject *error_value{}; - PyObject *error_traceback{}; - /* Save the current exception, if any. */ - PyErr_Fetch(&error_type, &error_value, &error_traceback); + Shiboken::Errors::Stash errorStash; if (canDelete) { if (sotp->is_multicpp) { @@ -441,7 +438,7 @@ static void SbkDeallocWrapperCommon(PyObject *pyObj, bool canDelete) } /* Restore the saved exception. */ - PyErr_Restore(error_type, error_value, error_traceback); + errorStash.restore(); if (needTypeDecref) Py_DECREF(pyType); @@ -454,7 +451,7 @@ static inline PyObject *_Sbk_NewVarObject(PyTypeObject *type) { // PYSIDE-1970: Support __slots__, implemented by PyVarObject auto const baseSize = sizeof(SbkObject); - auto varCount = Py_SIZE(type); + auto varCount = Py_SIZE(reinterpret_cast(type)); auto *self = PyObject_GC_NewVar(PyObject, type, varCount); if (varCount) std::memset(reinterpret_cast(self) + baseSize, 0, varCount * sizeof(void *)); @@ -539,9 +536,10 @@ PyObject *MakeQAppWrapper(PyTypeObject *type) // monitoring the last application state PyObject *qApp_curr = type != nullptr ? _Sbk_NewVarObject(type) : Py_None; - static PyObject *builtins = PyEval_GetBuiltins(); - if (PyDict_SetItem(builtins, Shiboken::PyName::qApp(), qApp_curr) < 0) + Shiboken::AutoDecRef builtins(PepEval_GetFrameBuiltins()); + if (PyDict_SetItem(builtins.object(), Shiboken::PyName::qApp(), qApp_curr) < 0) return nullptr; + builtins.reset(nullptr); qApp_last = qApp_curr; // Note: This Py_INCREF would normally be wrong because the qApp // object already has a reference from PyObject_GC_New. But this is @@ -731,7 +729,8 @@ PyObject *FallbackRichCompare(PyObject *self, PyObject *other, int op) bool SbkObjectType_Check(PyTypeObject *type) { static auto *meta = SbkObjectType_TypeF(); - return Py_TYPE(type) == meta || PyType_IsSubtype(Py_TYPE(type), meta); + auto *obType = reinterpret_cast(type); + return Py_TYPE(obType) == meta || PyType_IsSubtype(Py_TYPE(obType), meta); } // Global functions from folding. @@ -763,28 +762,89 @@ PyObject *Sbk_ReturnFromPython_Self(PyObject *self) return self; } -// The virtual function call -PyObject *Sbk_GetPyOverride(const void *voidThis, Shiboken::GilState &gil, const char *funcName, - bool *resultCache, PyObject **nameCache) -{ - PyObject *pyOverride{}; - if (!*resultCache) { - gil.acquire(); - pyOverride = Shiboken::BindingManager::instance().getOverride(voidThis, nameCache, funcName); - if (pyOverride == nullptr) { - *resultCache = true; - gil.release(); - } else if (Shiboken::Errors::occurred() != nullptr) { - // Give up. - Py_XDECREF(pyOverride); - pyOverride = nullptr; - } +} //extern "C" + +// Determine name of a Python override of a virtual method according to features +// and populate name cache. +static PyObject *overrideMethodName(PyObject *pySelf, const char *methodName, + PyObject **nameCache) +{ + // PYSIDE-1626: Touch the type to initiate switching early. + auto *obType = Py_TYPE(pySelf); + SbkObjectType_UpdateFeature(obType); + + const int flag = currentSelectId(obType); + const int propFlag = isdigit(methodName[0]) ? methodName[0] - '0' : 0; + const bool is_snake = flag & 0x01; + PyObject *pyMethodName = nameCache[is_snake]; // borrowed + if (pyMethodName == nullptr) { + if (propFlag) + methodName += 2; // skip the propFlag and ':' + pyMethodName = Shiboken::String::getSnakeCaseName(methodName, is_snake); + nameCache[is_snake] = pyMethodName; } - return pyOverride; + return pyMethodName; } -} //extern "C" +// The virtual function call +PyObject *Sbk_GetPyOverride(const void *voidThis, PyTypeObject *typeObject, + Shiboken::GilState &gil, const char *funcName, + PyObject *&resultCache, PyObject **nameCache) +{ + if (Py_IsInitialized() == 0 || resultCache == Py_None) + return nullptr; // Bail out, execute C++ call (wrappers may outlive Python). + + auto &bindingManager = Shiboken::BindingManager::instance(); + SbkObject *wrapper = bindingManager.retrieveWrapper(voidThis, typeObject); + // The refcount can be 0 if the object is dieing and someone called + // a virtual method from the destructor + if (wrapper == nullptr) + return nullptr; + auto *pySelf = reinterpret_cast(wrapper); + if (Py_REFCNT(pySelf) == 0) + return nullptr; + + gil.acquire(); + if (resultCache == Py_None) { // PYSIDE 3246, some other thread may have determined the override + gil.release(); + return nullptr; + } + + if (resultCache != nullptr) // recreate the callable from function/self + return PepExt_Type_CallDescrGet(resultCache, pySelf, nullptr); + + PyObject *pyMethodName = overrideMethodName(pySelf, funcName, nameCache); + auto *wrapper_dict = SbkObject_GetDict_NoRef(pySelf); + + // Note: This special case was implemented for duck-punching, which happens + // in the instance dict. It does not work with properties. + // This is not cached to avoid leaking. FIXME PYSIDE 7: Remove (PYSIDE-2916)? + if (PyObject *method = PyDict_GetItem(wrapper_dict, pyMethodName)) { + Py_INCREF(method); + return method; + } + + auto *pyOverride = Shiboken::BindingManager::getOverride(wrapper, pyMethodName); + if (pyOverride == nullptr) { + resultCache = Py_None; + Py_INCREF(resultCache); + gil.release(); + return nullptr; // No override, execute C++ call + } + + if (Shiboken::Errors::occurred() != nullptr) { + // Give up. + Py_XDECREF(pyOverride); + resultCache = Py_None; + Py_INCREF(resultCache); + gil.release(); + return nullptr; // // Give up. + } + resultCache = pyOverride; + // recreate the callable from function/self + return PepExt_Type_CallDescrGet(resultCache, pySelf, nullptr); +} namespace { @@ -815,7 +875,7 @@ void _initMainThreadId(); // helper.cpp static std::string msgFailedToInitializeType(const char *description) { std::ostringstream stream; - stream << "[libshiboken] Failed to initialize " << description; + stream << "libshiboken: Failed to initialize " << description; if (auto *error = PepErr_GetRaisedException()) { if (auto *str = PyObject_Str(error)) stream << ": " << Shiboken::String::toCString(str); @@ -830,7 +890,7 @@ namespace Conversions { void init(); } void init() { static bool shibokenAlreadInitialised = false; - if (shibokenAlreadInitialised) + if (shibokenAlreadInitialised) // Leave guard in place until fully ported to multi phase init return; _initMainThreadId(); @@ -857,17 +917,18 @@ void init() // PYSIDE-1735: Initialize the whole Shiboken startup. void initShibokenSupport(PyObject *module) { - Py_INCREF(SbkObject_TypeF()); - PyModule_AddObject(module, "Object", reinterpret_cast(SbkObject_TypeF())); + auto *type = SbkObject_TypeF(); + auto *obType = reinterpret_cast(type); + Py_INCREF(obType); + PepModule_AddType(module, type); // PYSIDE-1735: When the initialization was moved into Shiboken import, this // Py_INCREF became necessary. No idea why. Py_INCREF(module); init_shibokensupport_module(); - auto *type = SbkObject_TypeF(); if (InitSignatureStrings(type, SbkObject_SignatureStrings) < 0) - Py_FatalError("Error in initShibokenSupport"); + Py_FatalError("libshiboken: Error in initShibokenSupport"); } // setErrorAboutWrongArguments now gets overload info from the signature module. @@ -1088,7 +1149,7 @@ introduceWrapperType(PyObject *enclosingObject, // PyModule_AddObject steals type's reference. Py_INCREF(ob_type); - if (PyModule_AddObject(enclosingObject, typeName, ob_type) != 0) { + if (PepModule_AddType(enclosingObject, type) != 0) { std::cerr << "Warning: " << __FUNCTION__ << " returns nullptr for " << typeName << '/' << originalName << " due to PyModule_AddObject(enclosingObject=" << enclosingObject << ", ob_type=" << ob_type << ") failing\n"; @@ -1239,8 +1300,7 @@ void callCppDestructors(SbkObject *pyObj) DestroyQApplication(); return; } - PyTypeObject *type = Py_TYPE(pyObj); - auto *sotp = PepType_SOTP(type); + auto *sotp = PepType_SOTP(Shiboken::pyType(pyObj)); if (sotp->is_multicpp) { callDestructor(getDestructorEntries(pyObj)); } else { @@ -1297,7 +1357,8 @@ void getOwnership(PyObject *pyObj) void releaseOwnership(SbkObject *self) { // skip if the ownership have already moved to c++ - auto *selfType = Py_TYPE(self); + auto *ob = reinterpret_cast(self); + auto *selfType = Py_TYPE(ob); if (!self->d->hasOwnership || Shiboken::Conversions::pythonTypeIsValueType(PepType_SOTP(selfType)->converter)) return; @@ -1306,7 +1367,7 @@ void releaseOwnership(SbkObject *self) // If We have control over object life if (self->d->containsCppWrapper) - Py_INCREF(reinterpret_cast(self)); // keep the python object alive until the wrapper destructor call + Py_INCREF(ob); // keep the python object alive until the wrapper destructor call else invalidate(self); // If I do not know when this object will die We need to invalidate this to avoid use after } @@ -1399,7 +1460,7 @@ void makeValid(SbkObject *self) void *cppPointer(SbkObject *pyObj, PyTypeObject *desiredType) { - PyTypeObject *pyType = Py_TYPE(pyObj); + PyTypeObject *pyType = Shiboken::pyType(pyObj); auto *sotp = PepType_SOTP(pyType); int idx = 0; if (sotp->is_multicpp) @@ -1411,7 +1472,7 @@ void *cppPointer(SbkObject *pyObj, PyTypeObject *desiredType) std::vector cppPointers(SbkObject *pyObj) { - int n = getNumberOfCppBaseClasses(Py_TYPE(pyObj)); + int n = getNumberOfCppBaseClasses(Shiboken::pyType(pyObj)); std::vector ptrs(n); for (int i = 0; i < n; ++i) ptrs[i] = pyObj->d->cptr[i]; @@ -1421,7 +1482,7 @@ std::vector cppPointers(SbkObject *pyObj) bool setCppPointer(SbkObject *sbkObj, PyTypeObject *desiredType, void *cptr) { - PyTypeObject *type = Py_TYPE(sbkObj); + PyTypeObject *type = Shiboken::pyType(sbkObj); int idx = 0; if (PepType_SOTP(type)->is_multicpp) idx = getTypeIndexOnHierarchy(type, desiredType); @@ -1439,11 +1500,12 @@ bool setCppPointer(SbkObject *sbkObj, PyTypeObject *desiredType, void *cptr) bool isValid(PyObject *pyObj) { - if (!pyObj || pyObj == Py_None - || PyType_Check(pyObj) != 0 - || Py_TYPE(Py_TYPE(pyObj)) != SbkObjectType_TypeF()) { + if (pyObj == nullptr || pyObj == Py_None || PyType_Check(pyObj) != 0) + return true; + + PyTypeObject *type = Py_TYPE(pyObj); + if (Py_TYPE(reinterpret_cast(type)) != SbkObjectType_TypeF()) return true; - } auto *priv = reinterpret_cast(pyObj)->d; @@ -1468,17 +1530,18 @@ bool isValid(SbkObject *pyObj, bool throwPyError) return false; SbkObjectPrivate *priv = pyObj->d; - if (!priv->cppObjectCreated && isUserType(reinterpret_cast(pyObj))) { + auto *ob = reinterpret_cast(pyObj); + if (!priv->cppObjectCreated && isUserType(ob)) { if (throwPyError) PyErr_Format(PyExc_RuntimeError, "Base constructor of the object (%s) not called.", - Py_TYPE(pyObj)->tp_name); + Py_TYPE(ob)->tp_name); return false; } if (!priv->validCppObject) { if (throwPyError) PyErr_Format(PyExc_RuntimeError, "Internal C++ object (%s) already deleted.", - (Py_TYPE(pyObj))->tp_name); + (Py_TYPE(ob))->tp_name); return false; } @@ -1498,7 +1561,7 @@ SbkObject *findColocatedChild(SbkObject *wrapper, const PyTypeObject *instanceType) { // Degenerate case, wrapper is the correct wrapper. - if (reinterpret_cast(Py_TYPE(wrapper)) == reinterpret_cast(instanceType)) + if (reinterpret_cast(Shiboken::pyType(wrapper)) == reinterpret_cast(instanceType)) return wrapper; if (!(wrapper->d && wrapper->d->cptr)) @@ -1514,7 +1577,8 @@ SbkObject *findColocatedChild(SbkObject *wrapper, if (!(child->d && child->d->cptr)) continue; if (child->d->cptr[0] == wrapper->d->cptr[0]) { - return reinterpret_cast(Py_TYPE(child)) == reinterpret_cast(instanceType) + auto *childType = Shiboken::pyType(child); + return reinterpret_cast(childType) == reinterpret_cast(instanceType) ? child : findColocatedChild(child, instanceType); } } @@ -1582,42 +1646,16 @@ PyObject *newObjectWithHeuristics(PyTypeObject *instanceType, PyObject *newObjectForType(PyTypeObject *instanceType, void *cptr, bool hasOwnership) { - bool shouldCreate = true; - bool shouldRegister = true; - SbkObject *self = nullptr; - auto &bindingManager = BindingManager::instance(); - // Some logic to ensure that colocated child field does not overwrite the parent - if (SbkObject *existingWrapper = bindingManager.retrieveWrapper(cptr)) { - self = findColocatedChild(existingWrapper, instanceType); - if (self) { - // Wrapper already registered for cptr. - // This should not ideally happen, binding code should know when a wrapper - // already exists and retrieve it instead. - shouldRegister = shouldCreate = false; - } else if (hasOwnership && - (!(Shiboken::Object::hasCppWrapper(existingWrapper) || - Shiboken::Object::hasOwnership(existingWrapper)))) { - // Old wrapper is likely junk, since we have ownership and it doesn't. - bindingManager.releaseWrapper(existingWrapper); - } else { - // Old wrapper may be junk caused by some bug in identifying object deletion - // but it may not be junk when a colocated field is accessed for an - // object which was not created by python (returned from c++ factory function). - // Hence we cannot release the wrapper confidently so we do not register. - shouldRegister = false; - } - } - - if (shouldCreate) { + SbkObject *self = bindingManager.retrieveWrapper(cptr, instanceType); + if (self != nullptr) { + Py_IncRef(reinterpret_cast(self)); + } else { self = reinterpret_cast(SbkObject_tp_new(instanceType, nullptr, nullptr)); self->d->cptr[0] = cptr; self->d->hasOwnership = hasOwnership; self->d->validCppObject = 1; - if (shouldRegister) - bindingManager.registerWrapper(self, cptr); - } else { - Py_IncRef(reinterpret_cast(self)); + bindingManager.registerWrapper(self, cptr); } return reinterpret_cast(self); } @@ -1735,8 +1773,14 @@ void setParent(PyObject *parent, PyObject *child) parent_->d->parentInfo = new ParentInfo; // do not re-add a child - if (child_->d->parentInfo && (child_->d->parentInfo->parent == parent_)) + if (child_->d->parentInfo && (child_->d->parentInfo->parent == parent_)) { + if (Shiboken::pyVerbose()) { + std::cerr << "Warning: Attempt to re-add child " + << child << '/' << Py_TYPE(child)->tp_name << " to parent " + << parent << '/' << Py_TYPE(parent)->tp_name << '\n'; + } return; + } } ParentInfo *pInfo = child_->d->parentInfo; @@ -1759,7 +1803,7 @@ void setParent(PyObject *parent, PyObject *child) parent_->d->parentInfo->children.insert(child_); // Add Parent ref - Py_INCREF(child_); + Py_INCREF(child); // Remove ownership child_->d->hasOwnership = false; @@ -1795,7 +1839,7 @@ void deallocData(SbkObject *self, bool cleanup) void setTypeUserData(SbkObject *wrapper, void *userData, DeleteUserDataFunc d_func) { - auto *type = Py_TYPE(wrapper); + auto *type = Shiboken::pyType(wrapper); auto *sotp = PepType_SOTP(type); if (sotp->user_data) sotp->d_func(sotp->user_data); @@ -1806,7 +1850,7 @@ void setTypeUserData(SbkObject *wrapper, void *userData, DeleteUserDataFunc d_fu void *getTypeUserData(SbkObject *wrapper) { - auto *type = Py_TYPE(wrapper); + auto *type = Shiboken::pyType(wrapper); return PepType_SOTP(type)->user_data; } @@ -1879,14 +1923,15 @@ void clearReferences(SbkObject *self) static std::vector getBases(SbkObject *self) { - return ObjectType::isUserType(Py_TYPE(self)) - ? getCppBaseClasses(Py_TYPE(self)) - : std::vector(1, Py_TYPE(self)); + auto *type = Shiboken::pyType(self); + return ObjectType::isUserType(type) + ? getCppBaseClasses(type) + : std::vector(1, type); } static bool isValueType(SbkObject *self) { - return PepType_SOTP(Py_TYPE(self))->type_behaviour == BEHAVIOUR_VALUETYPE; + return PepType_SOTP(Shiboken::pyType(self))->type_behaviour == BEHAVIOUR_VALUETYPE; } void _debugFormat(std::ostream &s, SbkObject *self) @@ -1929,6 +1974,7 @@ std::string info(SbkObject *self) { std::ostringstream s; + s << "id................ " << self << '\n'; if (self->d && self->d->cptr) { const std::vector bases = getBases(self); @@ -1946,12 +1992,12 @@ std::string info(SbkObject *self) "validCppObject.... " << self->d->validCppObject << "\n" "wasCreatedByPython " << self->d->cppObjectCreated << "\n" "value...... " << isValueType(self) << "\n" - "reference count... " << reinterpret_cast(self)->ob_refcnt << '\n'; + "reference count... " << Py_REFCNT(reinterpret_cast(self)) << '\n'; if (self->d->parentInfo && self->d->parentInfo->parent) { s << "parent............ "; Shiboken::AutoDecRef parent(PyObject_Str(reinterpret_cast(self->d->parentInfo->parent))); - s << String::toCString(parent) << "\n"; + s << String::toCString(parent) << '\n'; } if (self->d->parentInfo && !self->d->parentInfo->children.empty()) { diff --git a/sources/shiboken6/libshiboken/basewrapper.h b/sources/shiboken6/libshiboken/basewrapper.h index 425328ae..426298bc 100644 --- a/sources/shiboken6/libshiboken/basewrapper.h +++ b/sources/shiboken6/libshiboken/basewrapper.h @@ -120,11 +120,12 @@ LIBSHIBOKEN_API bool SbkObjectType_Check(PyTypeObject *type); LIBSHIBOKEN_API PyObject *Sbk_ReturnFromPython_None(); LIBSHIBOKEN_API PyObject *Sbk_ReturnFromPython_Result(PyObject *pyResult); LIBSHIBOKEN_API PyObject *Sbk_ReturnFromPython_Self(PyObject *self); -LIBSHIBOKEN_API PyObject *Sbk_GetPyOverride(const void *voidThis, Shiboken::GilState &gil, - const char *funcName, bool *resultCache, - PyObject **nameCache); } // extern "C" +LIBSHIBOKEN_API PyObject *Sbk_GetPyOverride(const void *voidThis, PyTypeObject *typeObject, + Shiboken::GilState &gil, const char *funcName, + PyObject *&resultCache, PyObject **nameCache); + namespace Shiboken { diff --git a/sources/shiboken6/libshiboken/basewrapper_p.h b/sources/shiboken6/libshiboken/basewrapper_p.h index fb914079..e8744ad2 100644 --- a/sources/shiboken6/libshiboken/basewrapper_p.h +++ b/sources/shiboken6/libshiboken/basewrapper_p.h @@ -162,6 +162,11 @@ void deallocData(SbkObject *self, bool doCleanup); void _debugFormat(std::ostream &str, SbkObject *self); } // namespace Object +inline PyTypeObject *pyType(SbkObject *sbo) +{ + return Py_TYPE(reinterpret_cast(sbo)); +} + } // namespace Shiboken #endif diff --git a/sources/shiboken6/libshiboken/bindingmanager.cpp b/sources/shiboken6/libshiboken/bindingmanager.cpp index a5f7dff6..32eb697d 100644 --- a/sources/shiboken6/libshiboken/bindingmanager.cpp +++ b/sources/shiboken6/libshiboken/bindingmanager.cpp @@ -9,6 +9,7 @@ #include "helper.h" #include "sbkfeature_base.h" #include "sbkmodule.h" +#include "sbkpep.h" #include "sbkstaticstrings.h" #include "sbkstring.h" @@ -47,7 +48,9 @@ struct std::hash { namespace Shiboken { -using WrapperMap = std::unordered_map; +// Mapping of C++ address to wrapper. We use a multimap to allow for co-located +// objects, which happens for example for the first field of a struct. +using WrapperMap = std::unordered_multimap; template class BaseGraph @@ -174,6 +177,9 @@ struct BindingManager::BindingManagerPrivate { Graph classHierarchy; DestructorEntries deleteInMainThread; + WrapperMap::const_iterator findSbkObject(const void *cptr, SbkObject *wrapper) const; + WrapperMap::const_iterator findByType(const void *cptr, PyTypeObject *desiredType) const; + bool releaseWrapper(void *cptr, SbkObject *wrapper, const int *bases = nullptr); bool releaseWrapperHelper(void *cptr, SbkObject *wrapper); @@ -181,14 +187,43 @@ struct BindingManager::BindingManagerPrivate { void assignWrapperHelper(SbkObject *wrapper, const void *cptr); }; -inline bool BindingManager::BindingManagerPrivate::releaseWrapperHelper(void *cptr, SbkObject *wrapper) +// Find wrapper map entry by Python instance +WrapperMap::const_iterator + BindingManager::BindingManagerPrivate::findSbkObject(const void *cptr, + SbkObject *wrapper) const +{ + const auto end = wrapperMapper.cend(); + auto it = wrapperMapper.find(cptr); + for (; it != end && it->first == cptr; ++it) { + if (it->second == wrapper) + return it; + } + return end; +} + +// Find wrapper map entry by Python type +WrapperMap::const_iterator + BindingManager::BindingManagerPrivate::findByType(const void *cptr, + PyTypeObject *desiredType) const +{ + const auto end = wrapperMapper.cend(); + auto it = wrapperMapper.find(cptr); + for (; it != end && it->first == cptr; ++it) { + auto *foundType = Py_TYPE(reinterpret_cast(it->second)); + if (foundType == desiredType || PyType_IsSubtype(foundType, desiredType) != 0) + return it; + } + return end; +} + +bool BindingManager::BindingManagerPrivate::releaseWrapperHelper(void *cptr, SbkObject *wrapper) { // The wrapper argument is checked to ensure that the correct wrapper is released. // Returns true if the correct wrapper is found and released. // If wrapper argument is NULL, no such check is performed. - auto iter = wrapperMapper.find(cptr); - if (iter != wrapperMapper.end() && (wrapper == nullptr || iter->second == wrapper)) { - wrapperMapper.erase(iter); + const auto it = wrapper != nullptr ? findSbkObject(cptr, wrapper) : wrapperMapper.find(cptr); + if (it != wrapperMapper.cend()) { + wrapperMapper.erase(it); return true; } return false; @@ -211,8 +246,8 @@ bool BindingManager::BindingManagerPrivate::releaseWrapper(void *cptr, SbkObject inline void BindingManager::BindingManagerPrivate::assignWrapperHelper(SbkObject *wrapper, const void *cptr) { - auto iter = wrapperMapper.find(cptr); - if (iter == wrapperMapper.end()) + const auto it = findSbkObject(cptr, wrapper); + if (it == wrapperMapper.cend()) wrapperMapper.insert(std::make_pair(cptr, wrapper)); } @@ -264,15 +299,21 @@ BindingManager &BindingManager::instance() { return singleton; } -bool BindingManager::hasWrapper(const void *cptr) +bool BindingManager::hasWrapper(const void *cptr) const { std::lock_guard guard(m_d->wrapperMapLock); return m_d->wrapperMapper.find(cptr) != m_d->wrapperMapper.end(); } +bool BindingManager::hasWrapper(const void *cptr, PyTypeObject *typeObject) const +{ + std::lock_guard guard(m_d->wrapperMapLock); + return m_d->findByType(cptr, typeObject) != m_d->wrapperMapper.cend(); +} + void BindingManager::registerWrapper(SbkObject *pyObj, void *cptr) { - auto *instanceType = Py_TYPE(pyObj); + auto *instanceType = Shiboken::pyType(pyObj); auto *d = PepType_SOTP(instanceType); if (!d) @@ -285,9 +326,9 @@ void BindingManager::registerWrapper(SbkObject *pyObj, void *cptr) void BindingManager::releaseWrapper(SbkObject *sbkObj) { - auto *sbkType = Py_TYPE(sbkObj); + auto *sbkType = Shiboken::pyType(sbkObj); auto *d = PepType_SOTP(sbkType); - int numBases = ((d && d->is_multicpp) ? getNumberOfCppBaseClasses(Py_TYPE(sbkObj)) : 1); + int numBases = ((d && d->is_multicpp) ? getNumberOfCppBaseClasses(sbkType) : 1); void **cptrs = sbkObj->d->cptr; const int *mi_offsets = d != nullptr ? d->mi_offsets : nullptr; @@ -310,7 +351,7 @@ void BindingManager::addToDeletionInMainThread(const DestructorEntry &e) m_d->deleteInMainThread.push_back(e); } -SbkObject *BindingManager::retrieveWrapper(const void *cptr) +SbkObject *BindingManager::retrieveWrapper(const void *cptr) const { std::lock_guard guard(m_d->wrapperMapLock); auto iter = m_d->wrapperMapper.find(cptr); @@ -319,96 +360,60 @@ SbkObject *BindingManager::retrieveWrapper(const void *cptr) return iter->second; } -PyObject *BindingManager::getOverride(const void *cptr, - PyObject *nameCache[], - const char *methodName) +SbkObject *BindingManager::retrieveWrapper(const void *cptr, PyTypeObject *typeObject) const { - SbkObject *wrapper = retrieveWrapper(cptr); - // The refcount can be 0 if the object is dieing and someone called - // a virtual method from the destructor - if (!wrapper || Py_REFCNT(reinterpret_cast(wrapper)) == 0) - return nullptr; - - // PYSIDE-1626: Touch the type to initiate switching early. - SbkObjectType_UpdateFeature(Py_TYPE(wrapper)); - - int flag = currentSelectId(Py_TYPE(wrapper)); - int propFlag = isdigit(methodName[0]) ? methodName[0] - '0' : 0; - bool is_snake = flag & 0x01; - PyObject *pyMethodName = nameCache[is_snake]; // borrowed - if (pyMethodName == nullptr) { - if (propFlag) - methodName += 2; // skip the propFlag and ':' - pyMethodName = Shiboken::String::getSnakeCaseName(methodName, is_snake); - nameCache[is_snake] = pyMethodName; - } + std::lock_guard guard(m_d->wrapperMapLock); + const auto it = m_d->findByType(cptr, typeObject); + return it != m_d->wrapperMapper.cend() ? it->second : nullptr; +} +PyObject *BindingManager::getOverride(SbkObject *wrapper, PyObject *pyMethodName) +{ auto *obWrapper = reinterpret_cast(wrapper); - auto *wrapper_dict = SbkObject_GetDict_NoRef(obWrapper); - if (PyObject *method = PyDict_GetItem(wrapper_dict, pyMethodName)) { - // Note: This special case was implemented for duck-punching, which happens - // in the instance dict. It does not work with properties. - Py_INCREF(method); - return method; - } - PyObject *method = PyObject_GetAttr(obWrapper, pyMethodName); + Shiboken::AutoDecRef method(PyObject_GetAttr(obWrapper, pyMethodName)); + if (method.isNull()) + return nullptr; PyObject *function = nullptr; // PYSIDE-1523: PyMethod_Check is not accepting compiled methods, we do this rather // crude check for them. - if (method) { - // PYSIDE-535: This macro is redefined in a compatible way in pep384 - if (PyMethod_Check(method)) { - if (PyMethod_GET_SELF(method) == obWrapper) { - function = PyMethod_GET_FUNCTION(method); - } else { - Py_DECREF(method); - method = nullptr; - } - } else if (isCompiledMethod(method)) { - PyObject *im_self = PyObject_GetAttr(method, PyName::im_self()); - // Not retaining a reference inline with what PyMethod_GET_SELF does. - Py_DECREF(im_self); - - if (im_self == obWrapper) { - function = PyObject_GetAttr(method, PyName::im_func()); - // Not retaining a reference inline with what PyMethod_GET_FUNCTION does. - Py_DECREF(function); - } else { - Py_DECREF(method); - method = nullptr; - } - } else { - Py_DECREF(method); - method = nullptr; - } + // PYSIDE-535: This macro is redefined in a compatible way in pep384 + if (PyMethod_Check(method) != 0) { + if (PyMethod_Self(method) != obWrapper) + return nullptr; + function = PyMethod_Function(method); + } else if (isCompiledMethod(method)) { + Shiboken::AutoDecRef im_self(PyObject_GetAttr(method, PyName::im_self())); + // Not retaining a reference inline with what PyMethod_GET_SELF does. + if (im_self.object() != obWrapper) + return nullptr; + function = PyObject_GetAttr(method, PyName::im_func()); + // Not retaining a reference inline with what PyMethod_GET_FUNCTION does. + Py_DECREF(function); + } else { + return nullptr; } - if (method != nullptr) { - PyObject *mro = Py_TYPE(wrapper)->tp_mro; - - bool defaultFound = false; - // The first class in the mro (index 0) is the class being checked and it should not be tested. - // The last class in the mro (size - 1) is the base Python object class which should not be tested also. - for (Py_ssize_t idx = 1, size = PyTuple_Size(mro); idx < size - 1; ++idx) { - auto *parent = reinterpret_cast(PyTuple_GetItem(mro, idx)); - AutoDecRef parentDict(PepType_GetDict(parent)); - if (parentDict) { - if (PyObject *defaultMethod = PyDict_GetItem(parentDict.object(), pyMethodName)) { - defaultFound = true; - if (function != defaultMethod) - return method; - } + PyObject *mro = Py_TYPE(obWrapper)->tp_mro; + bool defaultFound = false; + // The first class in the mro (index 0) is the class being checked and it should not be tested. + // The last class in the mro (size - 1) is the base Python object class which should not be tested also. + for (Py_ssize_t idx = 1, size = PyTuple_Size(mro); idx < size - 1; ++idx) { + auto *parent = reinterpret_cast(PyTuple_GetItem(mro, idx)); + AutoDecRef parentDict(PepType_GetDict(parent)); + if (parentDict) { + if (PyObject *defaultMethod = PyDict_GetItem(parentDict.object(), pyMethodName)) { + defaultFound = true; + if (function != defaultMethod) + return function; } } - // PYSIDE-2255: If no default method was found, use the method. - if (!defaultFound) - return method; - Py_DECREF(method); } - + // PYSIDE-2255: If no default method was found, use the method. + if (!defaultFound) + return function; return nullptr; } @@ -448,7 +453,7 @@ void BindingManager::visitAllPyObjects(ObjectVisitor visitor, void *data) { WrapperMap copy = m_d->wrapperMapper; for (const auto &p : copy) { - if (hasWrapper(p.first)) + if (m_d->findSbkObject(p.first, p.second) != m_d->wrapperMapper.cend()) visitor(p.second, data); } } @@ -465,11 +470,11 @@ void BindingManager::dumpWrapperMap() << "WrapperMap size: " << wrapperMap.size() << " Types: " << m_d->classHierarchy.nodeSet().size() << '\n'; for (auto it : wrapperMap) { - const SbkObject *sbkObj = it.second; + auto *ob = reinterpret_cast(it.second); std::cerr << "key: " << it.first << ", value: " - << static_cast(sbkObj) << " (" - << (Py_TYPE(sbkObj))->tp_name << ", refcnt: " - << Py_REFCNT(reinterpret_cast(sbkObj)) << ")\n"; + << static_cast(ob) << " (" + << PepType_GetFullyQualifiedNameStr(Py_TYPE(ob)) << ", refcnt: " + << Py_REFCNT(ob) << ")\n"; } std::cerr << "-------------------------------\n"; } @@ -501,7 +506,7 @@ static bool _callInheritedInit(PyObject *self, PyObject *args, PyObject *kwds, /* No need to check the last one: it's gonna be skipped anyway. */ for ( ; idx + 1 < n; ++idx) { auto *lookType = reinterpret_cast(PyTuple_GetItem(mro, idx)); - if (className == lookType->tp_name) + if (className == PepType_GetFullyQualifiedNameStr(lookType)) break; } // We are now at the first non-Python class `QObject`. diff --git a/sources/shiboken6/libshiboken/bindingmanager.h b/sources/shiboken6/libshiboken/bindingmanager.h index 300783e7..e2a4ac8e 100644 --- a/sources/shiboken6/libshiboken/bindingmanager.h +++ b/sources/shiboken6/libshiboken/bindingmanager.h @@ -33,7 +33,8 @@ public: static BindingManager &instance(); - bool hasWrapper(const void *cptr); + bool hasWrapper(const void *cptr, PyTypeObject *typeObject) const; + bool hasWrapper(const void *cptr) const; void registerWrapper(SbkObject *pyObj, void *cptr); void releaseWrapper(SbkObject *wrapper); @@ -41,8 +42,9 @@ public: void runDeletionInMainThread(); void addToDeletionInMainThread(const DestructorEntry &); - SbkObject *retrieveWrapper(const void *cptr); - PyObject *getOverride(const void *cptr, PyObject *nameCache[], const char *methodName); + SbkObject *retrieveWrapper(const void *cptr, PyTypeObject *typeObject) const; + SbkObject *retrieveWrapper(const void *cptr) const; + static PyObject *getOverride(SbkObject *wrapper, PyObject *pyMethodName); void addClassInheritance(Module::TypeInitStruct *parent, Module::TypeInitStruct *child); /// Try to find the correct type of cptr via type discovery knowing that it's at least diff --git a/sources/shiboken6/libshiboken/bufferprocs_py37.cpp b/sources/shiboken6/libshiboken/bufferprocs_py37.cpp index 4ccf970e..c7f7648b 100644 --- a/sources/shiboken6/libshiboken/bufferprocs_py37.cpp +++ b/sources/shiboken6/libshiboken/bufferprocs_py37.cpp @@ -9,10 +9,12 @@ * */ -#ifdef Py_LIMITED_API +#include "bufferprocs_py37.h" +#include "sbkpep.h" -#include "sbkpython.h" -/* Buffer C-API for Python 3.0 */ +#if defined(Py_LIMITED_API) && Py_LIMITED_API < 0x030B0000 + +// Buffer C-API for Python 3.0 (copy of cpython/Objects/abstract.c:426) int PyObject_GetBuffer(PyObject *obj, Pep_buffer *view, int flags) @@ -28,320 +30,10 @@ PyObject_GetBuffer(PyObject *obj, Pep_buffer *view, int flags) return (*pb->bf_getbuffer)(obj, view, flags); } -static int -_IsFortranContiguous(const Pep_buffer *view) -{ - Py_ssize_t sd, dim; - int i; - - /* 1) len = product(shape) * itemsize - 2) itemsize > 0 - 3) len = 0 <==> exists i: shape[i] = 0 */ - if (view->len == 0) return 1; - if (view->strides == NULL) { /* C-contiguous by definition */ - /* Trivially F-contiguous */ - if (view->ndim <= 1) return 1; - - /* ndim > 1 implies shape != NULL */ - assert(view->shape != NULL); - - /* Effectively 1-d */ - sd = 0; - for (i=0; indim; i++) { - if (view->shape[i] > 1) sd += 1; - } - return sd <= 1; - } - - /* strides != NULL implies both of these */ - assert(view->ndim > 0); - assert(view->shape != NULL); - - sd = view->itemsize; - for (i=0; indim; i++) { - dim = view->shape[i]; - if (dim > 1 && view->strides[i] != sd) { - return 0; - } - sd *= dim; - } - return 1; -} - -static int -_IsCContiguous(const Pep_buffer *view) -{ - Py_ssize_t sd, dim; - int i; - - /* 1) len = product(shape) * itemsize - 2) itemsize > 0 - 3) len = 0 <==> exists i: shape[i] = 0 */ - if (view->len == 0) return 1; - if (view->strides == NULL) return 1; /* C-contiguous by definition */ - - /* strides != NULL implies both of these */ - assert(view->ndim > 0); - assert(view->shape != NULL); - - sd = view->itemsize; - for (i=view->ndim-1; i>=0; i--) { - dim = view->shape[i]; - if (dim > 1 && view->strides[i] != sd) { - return 0; - } - sd *= dim; - } - return 1; -} - -int -PyBuffer_IsContiguous(const Pep_buffer *view, char order) -{ - - if (view->suboffsets != NULL) return 0; - - if (order == 'C') - return _IsCContiguous(view); - else if (order == 'F') - return _IsFortranContiguous(view); - else if (order == 'A') - return (_IsCContiguous(view) || _IsFortranContiguous(view)); - return 0; -} - - -void * -PyBuffer_GetPointer(Pep_buffer *view, Py_ssize_t *indices) -{ - int i; - auto pointer = reinterpret_cast(view->buf); - for (i = 0; i < view->ndim; i++) { - pointer += view->strides[i]*indices[i]; - if ((view->suboffsets != NULL) && (view->suboffsets[i] >= 0)) { - pointer = *reinterpret_cast(pointer) + view->suboffsets[i]; - } - } - return pointer; -} - - -void -_Py_add_one_to_index_F(int nd, Py_ssize_t *index, const Py_ssize_t *shape) -{ - int k; - - for (k=0; k=0; k--) { - if (index[k] < shape[k]-1) { - index[k]++; - break; - } - else { - index[k] = 0; - } - } -} - -int -PyBuffer_FromContiguous(Pep_buffer *view, void *buf, Py_ssize_t len, char fort) -{ - int k; - void (*addone)(int, Py_ssize_t *, const Py_ssize_t *); - Py_ssize_t *indices, elements; - char *src, *ptr; - - if (len > view->len) { - len = view->len; - } - - if (PyBuffer_IsContiguous(view, fort)) { - /* simplest copy is all that is needed */ - memcpy(view->buf, buf, len); - return 0; - } - - /* Otherwise a more elaborate scheme is needed */ - - /* view->ndim <= 64 */ - indices = (Py_ssize_t *)PyMem_Malloc(sizeof(Py_ssize_t)*(view->ndim)); - if (indices == NULL) { - PyErr_NoMemory(); - return -1; - } - for (k=0; kndim; k++) { - indices[k] = 0; - } - - if (fort == 'F') { - addone = _Py_add_one_to_index_F; - } - else { - addone = _Py_add_one_to_index_C; - } - src = (char *)buf; // patched by CT - /* XXX : This is not going to be the fastest code in the world - several optimizations are possible. - */ - elements = len / view->itemsize; - while (elements--) { - ptr = (char *)PyBuffer_GetPointer(view, indices); // patched by CT - memcpy(ptr, src, view->itemsize); - src += view->itemsize; - addone(view->ndim, indices, view->shape); - } - - PyMem_Free(indices); - return 0; -} - -int PyObject_CopyData(PyObject *dest, PyObject *src) -{ - Pep_buffer view_dest, view_src; - int k; - Py_ssize_t *indices, elements; - char *dptr, *sptr; - - if (!PyObject_CheckBuffer(dest) || - !PyObject_CheckBuffer(src)) { - PyErr_SetString(PyExc_TypeError, - "both destination and source must be "\ - "bytes-like objects"); - return -1; - } - - if (PyObject_GetBuffer(dest, &view_dest, PyBUF_FULL) != 0) return -1; - if (PyObject_GetBuffer(src, &view_src, PyBUF_FULL_RO) != 0) { - PyBuffer_Release(&view_dest); - return -1; - } - - if (view_dest.len < view_src.len) { - PyErr_SetString(PyExc_BufferError, - "destination is too small to receive data from source"); - PyBuffer_Release(&view_dest); - PyBuffer_Release(&view_src); - return -1; - } - - if ((PyBuffer_IsContiguous(&view_dest, 'C') && - PyBuffer_IsContiguous(&view_src, 'C')) || - (PyBuffer_IsContiguous(&view_dest, 'F') && - PyBuffer_IsContiguous(&view_src, 'F'))) { - /* simplest copy is all that is needed */ - memcpy(view_dest.buf, view_src.buf, view_src.len); - PyBuffer_Release(&view_dest); - PyBuffer_Release(&view_src); - return 0; - } - - /* Otherwise a more elaborate copy scheme is needed */ - - /* XXX(nnorwitz): need to check for overflow! */ - indices = (Py_ssize_t *)PyMem_Malloc(sizeof(Py_ssize_t)*view_src.ndim); - if (indices == NULL) { - PyErr_NoMemory(); - PyBuffer_Release(&view_dest); - PyBuffer_Release(&view_src); - return -1; - } - for (k=0; k=0; k--) { - strides[k] = sd; - sd *= shape[k]; - } - } - return; -} - -int -PyBuffer_FillInfo(Pep_buffer *view, PyObject *obj, void *buf, Py_ssize_t len, - int readonly, int flags) -{ - if (view == NULL) { - PyErr_SetString(PyExc_BufferError, - "PyBuffer_FillInfo: view==NULL argument is obsolete"); - return -1; - } - - if (((flags & PyBUF_WRITABLE) == PyBUF_WRITABLE) && - (readonly == 1)) { - PyErr_SetString(PyExc_BufferError, - "Object is not writable."); - return -1; - } - - view->obj = obj; - if (obj) - Py_INCREF(obj); - view->buf = buf; - view->len = len; - view->readonly = readonly; - view->itemsize = 1; - view->format = NULL; - if ((flags & PyBUF_FORMAT) == PyBUF_FORMAT) - view->format = (char *)"B"; // patched by CT - view->ndim = 1; - view->shape = NULL; - if ((flags & PyBUF_ND) == PyBUF_ND) - view->shape = &(view->len); - view->strides = NULL; - if ((flags & PyBUF_STRIDES) == PyBUF_STRIDES) - view->strides = &(view->itemsize); - view->suboffsets = NULL; - view->internal = NULL; - return 0; -} +// Omitted functions: _IsFortranContiguous(), _IsCContiguous(), PyBuffer_IsContiguous(), +// PyBuffer_GetPointer(),// _Py_add_one_to_index_F(), _Py_add_one_to_index_C(), +// PyBuffer_FromContiguous(), PyObject_CopyData(), PyBuffer_FillContiguousStrides(), +// PyBuffer_FillInfo() void PyBuffer_Release(Pep_buffer *view) @@ -357,4 +49,4 @@ PyBuffer_Release(Pep_buffer *view) Py_DECREF(obj); } -#endif // Py_LIMITED_API +#endif // Py_LIMITED_API && < 3.11 diff --git a/sources/shiboken6/libshiboken/bufferprocs_py37.h b/sources/shiboken6/libshiboken/bufferprocs_py37.h index e16194e5..f381369e 100644 --- a/sources/shiboken6/libshiboken/bufferprocs_py37.h +++ b/sources/shiboken6/libshiboken/bufferprocs_py37.h @@ -50,9 +50,18 @@ PSF LICENSE AGREEMENT FOR PYTHON 3.7.0 #ifndef BUFFER_REENABLE_H #define BUFFER_REENABLE_H -/* buffer interface */ -// This has been renamed to Pep_buffer and will be used. -typedef struct bufferinfo { +#include "sbkpython.h" +#include "shibokenmacros.h" + +#ifdef Py_LIMITED_API + +// The buffer interface has been added to limited API in 3.11, (abstract.h, PYSIDE-1960, +// except some internal structs). + +# if Py_LIMITED_API < 0x030B0000 + +struct Pep_buffer +{ void *buf; PyObject *obj; /* owned reference */ Py_ssize_t len; @@ -65,10 +74,33 @@ typedef struct bufferinfo { Py_ssize_t *strides; Py_ssize_t *suboffsets; void *internal; -} Pep_buffer; +}; using getbufferproc =int (*)(PyObject *, Pep_buffer *, int); using releasebufferproc = void (*)(PyObject *, Pep_buffer *); +using Py_buffer = Pep_buffer; + +# else // < 3.11 + +using Pep_buffer = Py_buffer; + +# endif // >= 3.11 + +// The structs below are not part of the limited API. +struct PepBufferProcs +{ + getbufferproc bf_getbuffer; + releasebufferproc bf_releasebuffer; +}; + +struct PepBufferType +{ + PyVarObject ob_base; + void *skip[17]; + PepBufferProcs *tp_as_buffer; +}; + +# if Py_LIMITED_API < 0x030B0000 /* Maximum number of dimensions */ #define PyBUF_MAX_NDIM 64 @@ -106,4 +138,26 @@ using releasebufferproc = void (*)(PyObject *, Pep_buffer *); LIBSHIBOKEN_API PyObject *PyMemoryView_FromBuffer(Pep_buffer *info); #define Py_buffer Pep_buffer +#define PyObject_CheckBuffer(obj) \ + ((PepType_AS_BUFFER(Py_TYPE(obj)) != NULL) && \ + (PepType_AS_BUFFER(Py_TYPE(obj))->bf_getbuffer != NULL)) + +LIBSHIBOKEN_API int PyObject_GetBuffer(PyObject *ob, Pep_buffer *view, int flags); +LIBSHIBOKEN_API void PyBuffer_Release(Pep_buffer *view); + +# endif // << 3.11 + +# define PepType_AS_BUFFER(type) \ +reinterpret_cast(type)->tp_as_buffer + +using PyBufferProcs = PepBufferProcs; + +#else // Py_LIMITED_API + +using PepBufferProcs = PyBufferProcs; + +# define PepType_AS_BUFFER(type) ((type)->tp_as_buffer) + +#endif // !Py_LIMITED_API + #endif // BUFFER_REENABLE_H diff --git a/sources/shiboken6/libshiboken/helper.cpp b/sources/shiboken6/libshiboken/helper.cpp index 92f70f15..016ba125 100644 --- a/sources/shiboken6/libshiboken/helper.cpp +++ b/sources/shiboken6/libshiboken/helper.cpp @@ -2,10 +2,12 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "helper.h" +#include "sbkpepbuffer.h" #include "basewrapper_p.h" #include "sbkstring.h" #include "sbkstaticstrings.h" #include "pep384impl.h" +#include "bufferprocs_py37.h" #include #include @@ -46,14 +48,14 @@ static bool verbose = false; static void formatTypeTuple(PyObject *t, const char *what, std::ostream &str); -static void formatPyTypeObject(const PyTypeObject *obj, std::ostream &str, bool verbose) +static void formatPyTypeObject(PyTypeObject *obj, std::ostream &str, bool verbose) { if (obj == nullptr) { str << '0'; return; } - str << '"' << obj->tp_name << '"'; + str << '"' << PepType_GetFullyQualifiedNameStr(obj) << '"'; if (verbose) { bool immutableType = false; str << ", 0x" << std::hex << obj->tp_flags << std::dec; @@ -110,7 +112,7 @@ static void formatPyTypeObject(const PyTypeObject *obj, std::ostream &str, bool if (!immutableType) { auto *underlying = reinterpret_cast(obj)->ob_type; if (underlying != nullptr && underlying != obj) { - str << ", underlying=\"" << underlying->tp_name << '"'; + str << ", underlying=\"" << PepType_GetFullyQualifiedNameStr(underlying) << '"'; } } } @@ -126,10 +128,12 @@ static void formatTypeTuple(PyObject *t, const char *what, std::ostream &str) if (i != 0) str << ", "; Shiboken::AutoDecRef item(PyTuple_GetItem(t, i)); - if (item.isNull()) + if (item.isNull()) { str << '0'; // Observed with non-ready types - else - str << '"' << reinterpret_cast(item.object())->tp_name << '"'; + } else { + str << '"' << PepType_GetFullyQualifiedNameStr(reinterpret_cast(item.object())) + << '"'; + } } str << '}'; } @@ -179,9 +183,13 @@ static void formatPyDict(PyObject *obj, std::ostream &str) Py_ssize_t pos = 0; str << '{'; while (PyDict_Next(obj, &pos, &key, &value) != 0) { - if (pos) + if (pos > 1) str << ", "; - str << Shiboken::debugPyObject(key) << '=' << Shiboken::debugPyObject(value); + if (PyUnicode_Check(key)) + str << '"' << Shiboken::String::toCString(key) << '"'; + else + str << Shiboken::debugPyObject(key); + str << ": " << Shiboken::debugPyObject(value); } str << '}'; } @@ -356,6 +364,8 @@ static void formatPyObjectHelper(PyObject *obj, std::ostream &str) formatPyFunction(obj, str); else if (PyMethod_Check(obj) != 0) formatPyMethod(obj, str); + else if (PyModule_Check(obj) != 0) + str << "Module \"" << PyModule_GetName(obj) << '"'; else if (PepCode_Check(obj) != 0) formatPyCodeObject(obj, str); else if (PySequence_Check(obj)) @@ -386,7 +396,7 @@ debugSbkObject::debugSbkObject(SbkObject *o) : m_object(o) { } -debugPyTypeObject::debugPyTypeObject(const PyTypeObject *o) : m_object(o) +debugPyTypeObject::debugPyTypeObject(PyTypeObject *o) : m_object(o) { } @@ -492,10 +502,16 @@ bool listToArgcArgv(PyObject *argList, int *argcIn, char ***argvIn, const char * auto *argv = new char *[1]; *argvIn = argv; *argcIn = 1; - if (PyObject *appName = PyDict_GetItem(PyEval_GetGlobals(), Shiboken::PyMagicName::file())) - argv[0] = strDup(Shiboken::String::toCString(appName)); - else - argv[0] = strDup(defaultAppName ? defaultAppName : "PySideApplication"); + + const char *appNameC = nullptr; + Shiboken::AutoDecRef globals(PepEval_GetFrameGlobals()); + if (!globals.isNull()) { + if (PyObject *appName = PyDict_GetItem(globals, Shiboken::PyMagicName::file())) + appNameC = Shiboken::String::toCString(appName); + } + if (appNameC == nullptr) + appNameC = defaultAppName ? defaultAppName : "PySideApplication"; + argv[0] = strDup(appNameC); return true; } diff --git a/sources/shiboken6/libshiboken/helper.h b/sources/shiboken6/libshiboken/helper.h index ccd80b2d..a0a09e6a 100644 --- a/sources/shiboken6/libshiboken/helper.h +++ b/sources/shiboken6/libshiboken/helper.h @@ -96,17 +96,12 @@ struct LIBSHIBOKEN_API debugSbkObject struct LIBSHIBOKEN_API debugPyTypeObject { - explicit debugPyTypeObject(const PyTypeObject *o); + explicit debugPyTypeObject(PyTypeObject *o); - const PyTypeObject *m_object; + PyTypeObject *m_object; }; -struct LIBSHIBOKEN_API debugPyBuffer -{ - explicit debugPyBuffer(const Py_buffer &b); - - const Py_buffer &m_buffer; -}; +struct debugPyBuffer; struct debugPyArrayObject { diff --git a/sources/shiboken6/libshiboken/pep384impl.cpp b/sources/shiboken6/libshiboken/pep384impl.cpp index bd7a4c51..09a6a6e8 100644 --- a/sources/shiboken6/libshiboken/pep384impl.cpp +++ b/sources/shiboken6/libshiboken/pep384impl.cpp @@ -3,7 +3,7 @@ #define PEP384_INTERN -#include "sbkpython.h" +#include "pep384impl.h" #include "autodecref.h" #include "sbkstaticstrings.h" #include "sbkstaticstrings_p.h" @@ -120,7 +120,7 @@ check_PyTypeObject_valid() Shiboken::AutoDecRef tpDict(PepType_GetDict(check)); auto *checkDict = tpDict.object(); if (false - || strcmp(probe_tp_name, check->tp_name) != 0 + || std::strcmp(probe_tp_name, check->tp_name) != 0 || probe_tp_basicsize != check->tp_basicsize || probe_tp_dealloc != check->tp_dealloc || probe_tp_repr != check->tp_repr @@ -148,7 +148,7 @@ check_PyTypeObject_valid() || probe_tp_bases != typetype->tp_bases || probe_tp_mro != typetype->tp_mro || Py_TPFLAGS_DEFAULT != (check->tp_flags & Py_TPFLAGS_DEFAULT)) - Py_FatalError("The structure of type objects has changed!"); + Py_FatalError("libshiboken: The structure of type objects has changed!"); Py_DECREF(checkObj); Py_DECREF(probe_tp_base_obj); Py_DECREF(w); @@ -407,6 +407,8 @@ const char *_PepUnicode_AsString(PyObject *str) Py_FatalError("Error in " AT); } PyObject *bytesStr = PyUnicode_AsEncodedString(str, "utf8", nullptr); + if (bytesStr == nullptr) + Py_FatalError("Error in " AT); PyObject *entry = PyDict_GetItemWithError(cstring_dict, bytesStr); if (entry == nullptr) { int e = PyDict_SetItem(cstring_dict, bytesStr, bytesStr); @@ -464,7 +466,7 @@ Pep_GetVerboseFlag() // Support for pyerrors.h -#if defined(Py_LIMITED_API) || PY_VERSION_HEX < 0x030C0000 +#ifdef PEP_OLD_ERR_API // Emulate PyErr_GetRaisedException() using the deprecated PyErr_Fetch()/PyErr_Store() PyObject *PepErr_GetRaisedException() { @@ -549,7 +551,7 @@ static PyTypeObject *dt_getCheck(const char *name) PyObject *op = PyObject_GetAttrString(PyDateTimeAPI->module, name); if (op == nullptr) { fprintf(stderr, "datetime.%s not found\n", name); - Py_FatalError("aborting"); + Py_FatalError("libshiboken: error initializing DateTime support, aborting"); } return reinterpret_cast(op); } @@ -563,10 +565,10 @@ init_DateTime(void) if (!initialized) { PyDateTimeAPI = (datetime_struc *)malloc(sizeof(datetime_struc)); if (PyDateTimeAPI == nullptr) - Py_FatalError("PyDateTimeAPI malloc error, aborting"); + Py_FatalError("libshiboken: PyDateTimeAPI malloc error, aborting"); PyDateTimeAPI->module = PyImport_ImportModule("datetime"); if (PyDateTimeAPI->module == nullptr) - Py_FatalError("datetime module not found, aborting"); + Py_FatalError("libshiboken: datetime module not found, aborting"); PyDateTimeAPI->DateType = dt_getCheck("date"); PyDateTimeAPI->DateTimeType = dt_getCheck("datetime"); PyDateTimeAPI->TimeType = dt_getCheck("time"); @@ -781,6 +783,22 @@ PepType_GetNameStr(PyTypeObject *type) return nodots != nullptr ? nodots + 1 : ret; } +const char *PepType_GetFullyQualifiedNameStr(PyTypeObject *type) +{ +#if 0 + // Should look like the below code for Limited API >= 3.13, however, it crashes for heap types + // since PyType_GetFullyQualifiedName() calls type_qualname() at Objects/typeobject.c:1402 + // which expects a PyHeapTypeObject (struct _heaptypeobject). + Shiboken::AutoDecRef name(PyType_GetFullyQualifiedName(type)); + Py_ssize_t size{}; + const char *result = PyUnicode_AsUTF8AndSize(name.object(), &size); + assert(result != nullptr && size > 0); + return result; +#else + return type->tp_name; +#endif +} + // PYSIDE-2264: Find the _functools or functools module and retrieve the // partial function. This can be tampered with, check carefully. PyObject * @@ -798,10 +816,10 @@ Pep_GetPartialFunction(void) functools = PyImport_ImportModule("functools"); } if (!functools) - Py_FatalError("functools cannot be found"); + Py_FatalError("libshiboken: functools cannot be found"); result = PyObject_GetAttrString(functools, "partial"); if (!result || !PyCallable_Check(result)) - Py_FatalError("partial not found or not a function"); + Py_FatalError("libshiboken: partial not found or not a function"); initialized = true; return result; } @@ -822,10 +840,14 @@ PepRun_GetResult(const char *command) * Evaluate a string and return the variable `result` */ PyObject *d = PyDict_New(); - if (d == nullptr - || PyDict_SetItem(d, Shiboken::PyMagicName::builtins(), PyEval_GetBuiltins()) < 0) { + if (d == nullptr) return nullptr; - } + + Shiboken::AutoDecRef builtins(PepEval_GetFrameBuiltins()); + if (PyDict_SetItem(d, Shiboken::PyMagicName::builtins(), PyEval_GetBuiltins()) < 0) + return nullptr; + builtins.reset(nullptr); + PyObject *v = PyRun_String(command, Py_file_input, d, d); PyObject *res = v ? PyDict_GetItem(d, Shiboken::PyName::result()) : nullptr; Py_XDECREF(v); @@ -1137,6 +1159,61 @@ void *PepType_GetSlot(PyTypeObject *type, int aSlot) return nullptr; } +PyObject *PepEval_GetFrameGlobals() +{ + // PyEval_GetFrameGlobals() (added to stable ABI in 3.13) returns a new reference + // as opposed to deprecated PyEval_GetGlobals() which returns a borrowed reference +#if !defined(PYPY_VERSION) && ((!defined(Py_LIMITED_API) && PY_VERSION_HEX >= 0x030D0000) || (defined(Py_LIMITED_API) && Py_LIMITED_API >= 0x030D0000)) + return PyEval_GetFrameGlobals(); +#else + PyObject *result = PyEval_GetGlobals(); + Py_XINCREF(result); + return result; +#endif +} + +PyObject *PepEval_GetFrameBuiltins() +{ + // PepEval_GetFrameBuiltins() (added to stable ABI in 3.13) returns a new reference + // as opposed to deprecated PyEval_GetBuiltins() which returns a borrowed reference +#if !defined(PYPY_VERSION) && ((!defined(Py_LIMITED_API) && PY_VERSION_HEX >= 0x030D0000) || (defined(Py_LIMITED_API) && Py_LIMITED_API >= 0x030D0000)) + return PyEval_GetFrameBuiltins(); +#else + PyObject *result = PyEval_GetBuiltins(); + Py_XINCREF(result); + return result; +#endif +} + +int PepModule_AddType(PyObject *module, PyTypeObject *type) +{ + // PyModule_AddType (added to stable ABI in 3.10) is the replacement for + // PyModule_AddObject() (deprecated in 3.13) for adding types to a module. +#if !defined(PYPY_VERSION) && ((!defined(Py_LIMITED_API) && PY_VERSION_HEX >= 0x030A0000) || (defined(Py_LIMITED_API) && Py_LIMITED_API >= 0x030A0000)) + return PyModule_AddType(module, type); +#else + auto *ob = reinterpret_cast(type); + int result = PyModule_AddObject(module, PepType_GetNameStr(type), ob); + if (result != 0) + Py_XDECREF(ob); + return result; +#endif +} + +int PepModule_Add(PyObject *module, const char *name, PyObject *value) +{ + // PyModule_Add (added to stable ABI in 3.13) is the replacement for PyModule_AddObject() + // (deprecated in 3.13). +#if !defined(PYPY_VERSION) && ((!defined(Py_LIMITED_API) && PY_VERSION_HEX >= 0x030D0000) || (defined(Py_LIMITED_API) && Py_LIMITED_API >= 0x030D0000)) + return PyModule_Add(module, name, value); +#else + int result = PyModule_AddObject(module, name, value); + if (result != 0) + Py_XDECREF(value); + return result; +#endif +} + /*************************************************************************** * * PYSIDE-535: The enum/flag error diff --git a/sources/shiboken6/libshiboken/pep384impl.h b/sources/shiboken6/libshiboken/pep384impl.h index 4c4e1b47..96284b93 100644 --- a/sources/shiboken6/libshiboken/pep384impl.h +++ b/sources/shiboken6/libshiboken/pep384impl.h @@ -4,22 +4,12 @@ #ifndef PEP384IMPL_H #define PEP384IMPL_H +#include "sbkpython.h" #include "shibokenmacros.h" extern "C" { -/***************************************************************************** - * - * RESOLVED: memoryobject.h - * - */ - -// Extracted into bufferprocs27.h -#ifdef Py_LIMITED_API -#include "bufferprocs_py37.h" -#endif - /***************************************************************************** * * RESOLVED: object.h @@ -170,8 +160,14 @@ struct SbkQFlagsTypePrivate; /*****************************************************************************/ // functions used everywhere + +/// (convenience) Return the unqualified type name LIBSHIBOKEN_API const char *PepType_GetNameStr(PyTypeObject *type); +/// (convenience) Return the fully qualified type name(PepType_GetFullyQualifiedNameStr()) +/// as C-string +LIBSHIBOKEN_API const char *PepType_GetFullyQualifiedNameStr(PyTypeObject *type); + LIBSHIBOKEN_API PyObject *Pep_GetPartialFunction(void); /***************************************************************************** @@ -190,15 +186,20 @@ LIBSHIBOKEN_API int Pep_GetFlag(const char *name); LIBSHIBOKEN_API int Pep_GetVerboseFlag(void); #endif +#if (defined(Py_LIMITED_API) && Py_LIMITED_API < 0x030C0000) || PY_VERSION_HEX < 0x030C0000 +# define PEP_OLD_ERR_API +#endif + // pyerrors.h -#if defined(Py_LIMITED_API) || PY_VERSION_HEX < 0x030C0000 +#ifdef PEP_OLD_ERR_API LIBSHIBOKEN_API PyObject *PepErr_GetRaisedException(); LIBSHIBOKEN_API PyObject *PepException_GetArgs(PyObject *ex); LIBSHIBOKEN_API void PepException_SetArgs(PyObject *ex, PyObject *args); #else -# define PepErr_GetRaisedException PyErr_GetRaisedException -# define PepException_GetArgs PyException_GetArgs -# define PepException_SetArgs PyException_SetArgs +inline PyObject *PepErr_GetRaisedException() { return PyErr_GetRaisedException(); } +inline PyObject *PepException_GetArgs(PyObject *ex) { return PyException_GetArgs(ex); } +inline void PepException_SetArgs(PyObject *ex, PyObject *args) +{ PyException_SetArgs(ex, args); } #endif /***************************************************************************** @@ -290,48 +291,6 @@ using PyCFunctionObject = struct _pycfunc; LIBSHIBOKEN_API PyObject *PyRun_String(const char *, int, PyObject *, PyObject *); #endif -/***************************************************************************** - * - * RESOLVED: abstract.h - * - */ -#ifdef Py_LIMITED_API - -// This definition breaks the limited API a little, because it re-enables the -// buffer functions. -// But this is no problem as we check it's validity for every version. - -// PYSIDE-1960 The buffer interface is since Python 3.11 part of the stable -// API and we do not need to check the compatibility by hand anymore. - -typedef struct { - getbufferproc bf_getbuffer; - releasebufferproc bf_releasebuffer; -} PyBufferProcs; - -typedef struct _Pepbuffertype { - PyVarObject ob_base; - void *skip[17]; - PyBufferProcs *tp_as_buffer; -} PepBufferType; - -#define PepType_AS_BUFFER(type) \ - reinterpret_cast(type)->tp_as_buffer - -#define PyObject_CheckBuffer(obj) \ - ((PepType_AS_BUFFER(Py_TYPE(obj)) != NULL) && \ - (PepType_AS_BUFFER(Py_TYPE(obj))->bf_getbuffer != NULL)) - -LIBSHIBOKEN_API int PyObject_GetBuffer(PyObject *ob, Pep_buffer *view, int flags); -LIBSHIBOKEN_API void PyBuffer_Release(Pep_buffer *view); - -#else - -#define Pep_buffer Py_buffer -#define PepType_AS_BUFFER(type) ((type)->tp_as_buffer) - -#endif /* Py_LIMITED_API */ - /***************************************************************************** * * RESOLVED: funcobject.h @@ -549,6 +508,18 @@ LIBSHIBOKEN_API int PepType_SetDict(PyTypeObject *type, PyObject *dict); LIBSHIBOKEN_API void *PepType_GetSlot(PyTypeObject *type, int aSlot); +// Runtime support for Python 3.13 stable ABI + +// Return dictionary of the global variables in the current execution frame +LIBSHIBOKEN_API PyObject *PepEval_GetFrameGlobals(); + +// Return a dictionary of the builtins in the current execution frame +LIBSHIBOKEN_API PyObject *PepEval_GetFrameBuiltins(); + +LIBSHIBOKEN_API int PepModule_AddType(PyObject *module, PyTypeObject *type); + +LIBSHIBOKEN_API int PepModule_Add(PyObject *module, const char *name, PyObject *value); + /***************************************************************************** * * Module Initialization diff --git a/sources/shiboken6/libshiboken/sbkbindingutils.cpp b/sources/shiboken6/libshiboken/sbkbindingutils.cpp index e01c4fc1..d61551b3 100644 --- a/sources/shiboken6/libshiboken/sbkbindingutils.cpp +++ b/sources/shiboken6/libshiboken/sbkbindingutils.cpp @@ -4,6 +4,9 @@ #include "sbkbindingutils.h" #include "autodecref.h" +#include "sbkstring.h" +#include "sbkpep.h" +#include "sbkstaticstrings_p.h" #include @@ -73,4 +76,26 @@ bool parseConstructorKeywordArguments(PyObject *kwds, return true; } +static bool isCompiledHelper() +{ + Shiboken::AutoDecRef globals(PepEval_GetFrameGlobals()); + if (globals.isNull()) + return false; + + if (PyDict_GetItem(globals.object(), PyMagicName::compiled()) != nullptr) + return true; + globals.reset(nullptr); + + // __compiled__ may not be set in initialization phases, check builtins + static PyObject *nuitkaDir = Shiboken::String::createStaticString("__nuitka_binary_exe"); + Shiboken::AutoDecRef builtins(PepEval_GetFrameBuiltins()); + return !builtins.isNull() && PyDict_GetItem(builtins.object(), nuitkaDir) != nullptr; +} + +bool isCompiled() +{ + static const bool result = isCompiledHelper(); + return result; +} + } // namespace Shiboken diff --git a/sources/shiboken6/libshiboken/sbkbindingutils.h b/sources/shiboken6/libshiboken/sbkbindingutils.h index 4ed833df..310fe342 100644 --- a/sources/shiboken6/libshiboken/sbkbindingutils.h +++ b/sources/shiboken6/libshiboken/sbkbindingutils.h @@ -35,6 +35,9 @@ LIBSHIBOKEN_API bool const ArgumentNameIndexMapping *mapping, size_t size, Shiboken::AutoDecRef &errInfo, PyObject **pyArgs); +/// Returns whether we are running in compiled mode (Nuitka). +LIBSHIBOKEN_API bool isCompiled(); + } // namespace Shiboken #endif // SBK_BINDINGUTILS diff --git a/sources/shiboken6/libshiboken/sbkcontainer.cpp b/sources/shiboken6/libshiboken/sbkcontainer.cpp index 7de1f03e..52eb419a 100644 --- a/sources/shiboken6/libshiboken/sbkcontainer.cpp +++ b/sources/shiboken6/libshiboken/sbkcontainer.cpp @@ -2,9 +2,25 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "sbkcontainer.h" +#include "sbkpep.h" #include "sbkstaticstrings.h" #include "autodecref.h" +// The functionality was moved to a source to reduce size +// and remove PepType_GetSlot() usage from the public header. +ShibokenContainer *ShibokenSequenceContainerPrivateBase::allocContainer(PyTypeObject *subtype) +{ + allocfunc allocFunc = reinterpret_cast(PepType_GetSlot(subtype, Py_tp_alloc)); + return reinterpret_cast(allocFunc(subtype, 0)); +} + +void ShibokenSequenceContainerPrivateBase::freeSelf(PyObject *pySelf) +{ + auto freeFunc = reinterpret_cast(PepType_GetSlot(Py_TYPE(pySelf)->tp_base, + Py_tp_free)); + freeFunc(pySelf); +} + namespace Shiboken { bool isOpaqueContainer(PyObject *o) diff --git a/sources/shiboken6/libshiboken/sbkcontainer.h b/sources/shiboken6/libshiboken/sbkcontainer.h index 8ad5aadc..083f994e 100644 --- a/sources/shiboken6/libshiboken/sbkcontainer.h +++ b/sources/shiboken6/libshiboken/sbkcontainer.h @@ -13,6 +13,8 @@ #include #include +// Opaque container helpers + extern "C" { struct LIBSHIBOKEN_API ShibokenContainer @@ -48,8 +50,35 @@ public: enum { value = sizeof(test(nullptr)) == sizeof(YesType) }; }; +// PYSIDE-3259 Handling of the std::vector optimization for providing +// a pointer for the SbkConverter. Use const-ref for the standard case to +// avoid copies and instantiate a bool in case of std::vector. +template +struct ShibokenContainerStdVectorValueType +{ + using Type = const T &; +}; + +template <> +struct ShibokenContainerStdVectorValueType +{ + using Type = bool; +}; + +class ShibokenSequenceContainerPrivateBase +{ +public: + static constexpr const char *msgModifyConstContainer = + "Attempt to modify a constant container."; + +protected: + LIBSHIBOKEN_API static ShibokenContainer *allocContainer(PyTypeObject *subtype); + LIBSHIBOKEN_API static void freeSelf(PyObject *pySelf); +}; + +// Helper for sequence type containers template -class ShibokenSequenceContainerPrivate // Helper for sequence type containers +class ShibokenSequenceContainerPrivate : public ShibokenSequenceContainerPrivateBase { public: using value_type = typename SequenceContainer::value_type; @@ -58,13 +87,10 @@ public: SequenceContainer *m_list{}; bool m_ownsList = false; bool m_const = false; - static constexpr const char *msgModifyConstContainer = - "Attempt to modify a constant container."; static PyObject *tpNew(PyTypeObject *subtype, PyObject * /* args */, PyObject * /* kwds */) { - allocfunc allocFunc = reinterpret_cast(PepType_GetSlot(subtype, Py_tp_alloc)); - auto *me = reinterpret_cast(allocFunc(subtype, 0)); + auto *me = allocContainer(subtype); auto *d = new ShibokenSequenceContainerPrivate; d->m_list = new SequenceContainer; d->m_ownsList = true; @@ -91,9 +117,7 @@ public: if (d->m_ownsList) delete d->m_list; delete d; - auto freeFunc = reinterpret_cast(PepType_GetSlot(Py_TYPE(pySelf)->tp_base, - Py_tp_free)); - freeFunc(self); + freeSelf(pySelf); } static Py_ssize_t sqLen(PyObject *self) diff --git a/sources/shiboken6/libshiboken/sbkconverter.cpp b/sources/shiboken6/libshiboken/sbkconverter.cpp index b8634002..9853e369 100644 --- a/sources/shiboken6/libshiboken/sbkconverter.cpp +++ b/sources/shiboken6/libshiboken/sbkconverter.cpp @@ -4,11 +4,12 @@ #include "sbkconverter.h" #include "sbkconverter_p.h" #include "sbkarrayconverter_p.h" -#include "sbkmodule.h" +#include "sbkmodule_p.h" #include "basewrapper_p.h" #include "bindingmanager.h" #include "autodecref.h" #include "helper.h" +#include "sbkpep.h" #include "voidptr.h" #include @@ -84,9 +85,9 @@ static void dumpPyTypeObject(std::ostream &str, PyTypeObject *t) str << ""; return; } - str << '"' << t->tp_name << '"'; + str << '"' << PepType_GetFullyQualifiedNameStr(t) << '"'; if (t->tp_base != nullptr && t->tp_base != &PyBaseObject_Type) - str << '(' << t->tp_base->tp_name << ')'; + str << '(' << PepType_GetFullyQualifiedNameStr(t->tp_base) << ')'; } static void dumpSbkConverter(std::ostream &str, const SbkConverter *c) @@ -183,10 +184,11 @@ SbkConverter *createConverterObject(PyTypeObject *type, auto *converter = new SbkConverter; converter->pythonType = type; // PYSIDE-595: All types are heaptypes now, so provide reference. - Py_XINCREF(type); + Py_XINCREF(reinterpret_cast(type)); converter->pointerToPython = pointerToPythonFunc; converter->copyToPython = copyToPythonFunc; + converter->copyToPythonWithType = nullptr; if (toCppPointerCheckFunc && toCppPointerConvFunc) converter->toCppPointerConversion = std::make_pair(toCppPointerCheckFunc, toCppPointerConvFunc); @@ -214,6 +216,13 @@ SbkConverter *createConverter(PyTypeObject *type, CppToPythonFunc toPythonFunc) return createConverterObject(type, nullptr, nullptr, nullptr, toPythonFunc); } +SbkConverter *createConverter(PyTypeObject *type, CppToPythonWithTypeFunc toPythonFunc) +{ + auto *result = createConverterObject(type, nullptr, nullptr, nullptr, nullptr); + result->copyToPythonWithType = toPythonFunc; + return result; +} + void deleteConverter(SbkConverter *converter) { if (converter) { @@ -293,8 +302,8 @@ PyObject *referenceToPython(const SbkConverter *converter, const void *cppIn) { assert(cppIn); - auto *pyOut = reinterpret_cast(BindingManager::instance().retrieveWrapper(cppIn)); - if (pyOut) { + if (auto *sbkOut = BindingManager::instance().retrieveWrapper(cppIn, converter->pythonType)) { + auto *pyOut = reinterpret_cast(sbkOut); Py_INCREF(pyOut); return pyOut; } @@ -310,12 +319,13 @@ static inline PyObject *CopyCppToPython(const SbkConverter *converter, const voi { if (!cppIn) Py_RETURN_NONE; - if (!converter->copyToPython) { - warning(PyExc_RuntimeWarning, 0, "CopyCppToPython(): SbkConverter::copyToPython is null for \"%s\".", - converter->pythonType->tp_name); - Py_RETURN_NONE; - } - return converter->copyToPython(cppIn); + if (converter->copyToPythonWithType != nullptr) + return converter->copyToPythonWithType(converter->pythonType, cppIn); + if (converter->copyToPython != nullptr) + return converter->copyToPython(cppIn); + warning(PyExc_RuntimeWarning, 0, "CopyCppToPython(): SbkConverter::copyToPython is null for \"%s\".", + converter->pythonType->tp_name); + Py_RETURN_NONE; } PyObject *copyToPython(PyTypeObject *type, const void *cppIn) @@ -451,9 +461,13 @@ void nonePythonToCppNullPtr(PyObject *, void *cppOut) void *cppPointer(PyTypeObject *desiredType, SbkObject *pyIn) { assert(pyIn); - if (!ObjectType::checkType(desiredType)) + if (!ObjectType::checkType(desiredType)) { + std::cerr << __FUNCTION__ << ": Conversion to non SbkObject type " + << PepType_GetFullyQualifiedNameStr(desiredType) + << " requested, falling back to pass-through.\n"; return pyIn; - auto *inType = Py_TYPE(pyIn); + } + auto *inType = Shiboken::pyType(pyIn); if (ObjectType::hasCast(inType)) return ObjectType::cast(inType, pyIn, desiredType); return Object::cppPointer(pyIn, desiredType); @@ -485,8 +499,14 @@ static void _pythonToCppCopy(const SbkConverter *converter, PyObject *pyIn, void assert(pyIn); assert(cppOut); PythonToCppFunc toCpp = IsPythonToCppConvertible(converter, pyIn); - if (toCpp) + if (toCpp) { toCpp(pyIn, cppOut); + } else { + std::cerr << __FUNCTION__ << ": Cannot copy-convert " << pyIn; + if (pyIn) + std::cerr << " (" << PepType_GetFullyQualifiedNameStr(Py_TYPE(pyIn)) << ')'; + std::cerr << " to C++.\n"; + } } void pythonToCppCopy(PyTypeObject *type, PyObject *pyIn, void *cppOut) @@ -575,7 +595,7 @@ SbkConverter *getConverter(const char *typeNameC) return it->second; // PYSIDE-2404: Did not find the name. Load the lazy classes // which have this name and try again. - Shiboken::Module::loadLazyClassesWithName(getRealTypeName(typeName).c_str()); + Shiboken::Module::loadLazyClassesWithNameStd(getRealTypeName(typeName)); it = converters.find(typeName); if (it != converters.end()) return it->second; @@ -856,18 +876,23 @@ PyTypeObject *getPythonTypeObject(const char *typeName) return getPythonTypeObject(getConverter(typeName)); } +static bool hasCopyToPythonFunc(const SbkConverter *converter) +{ + return converter->copyToPython != nullptr || converter->copyToPythonWithType != nullptr; +} + bool pythonTypeIsValueType(const SbkConverter *converter) { // Unlikely to happen but for multi-inheritance SbkObjs // the converter is not defined, hence we need a default return. if (!converter) return false; - return converter->pointerToPython && converter->copyToPython; + return converter->pointerToPython && hasCopyToPythonFunc(converter); } bool pythonTypeIsObjectType(const SbkConverter *converter) { - return converter->pointerToPython && !converter->copyToPython; + return converter->pointerToPython && !hasCopyToPythonFunc(converter); } bool pythonTypeIsWrapperType(const SbkConverter *converter) @@ -881,7 +906,7 @@ SpecificConverter::SpecificConverter(const char *typeName) m_converter = getConverter(typeName); if (!m_converter) return; - const auto len = strlen(typeName); + const auto len = std::strlen(typeName); char lastChar = typeName[len -1]; if (lastChar == '&') { m_type = ReferenceConversion; diff --git a/sources/shiboken6/libshiboken/sbkconverter.h b/sources/shiboken6/libshiboken/sbkconverter.h index a050844f..e774fa01 100644 --- a/sources/shiboken6/libshiboken/sbkconverter.h +++ b/sources/shiboken6/libshiboken/sbkconverter.h @@ -45,6 +45,11 @@ struct SbkArrayConverter; */ using CppToPythonFunc = PyObject *(*)(const void *); +/** Same as CppToPythonFunc, but additionally receives the 'PyTypeObject *'. + * This is handy for some converters, namely enumeration converters or + * dynamic user-defined converters that invoke the type object. */ +using CppToPythonWithTypeFunc = PyObject *(*)(PyTypeObject *, const void *); + /** * This function converts a Python object to a C++ value, it may be * a pointer, value, class, container or primitive type, passed via @@ -126,6 +131,7 @@ LIBSHIBOKEN_API SbkConverter *createConverter(PyTypeObject *type, * \returns A new type converter. */ LIBSHIBOKEN_API SbkConverter *createConverter(PyTypeObject *type, CppToPythonFunc toPythonFunc); +LIBSHIBOKEN_API SbkConverter *createConverter(PyTypeObject *type, CppToPythonWithTypeFunc toPythonFunc); LIBSHIBOKEN_API void deleteConverter(SbkConverter *converter); diff --git a/sources/shiboken6/libshiboken/sbkconverter_p.h b/sources/shiboken6/libshiboken/sbkconverter_p.h index b4ef51d4..974b0e4b 100644 --- a/sources/shiboken6/libshiboken/sbkconverter_p.h +++ b/sources/shiboken6/libshiboken/sbkconverter_p.h @@ -50,6 +50,10 @@ struct SbkConverter * wrapper assigned for it. */ CppToPythonFunc copyToPython; + /** Same as copyToPython, but additionally receives the 'PyTypeObject *'. + * Both functions are checked. + * FIXME PYSIDE 7: Add PyTypeObject parameter to CppToPythonFunc? */ + CppToPythonWithTypeFunc copyToPythonWithType; /** * This is a special case of a Python to C++ conversion. It returns * the underlying C++ pointer of a Python wrapper passed as parameter @@ -366,7 +370,7 @@ struct Primitive : OnePrimitive } static void toCpp(PyObject *pyIn, void *cppOut) { - *reinterpret_cast(cppOut) = PyLong_AS_LONG(pyIn) != 0; + *reinterpret_cast(cppOut) = PyLong_AsLong(pyIn) != 0; } }; diff --git a/sources/shiboken6/libshiboken/sbkcppstring.cpp b/sources/shiboken6/libshiboken/sbkcppstring.cpp index 1aec93f1..53f0de42 100644 --- a/sources/shiboken6/libshiboken/sbkcppstring.cpp +++ b/sources/shiboken6/libshiboken/sbkcppstring.cpp @@ -3,6 +3,7 @@ #include "sbkcppstring.h" #include "autodecref.h" +#include "sbkpep.h" namespace Shiboken::String { diff --git a/sources/shiboken6/libshiboken/sbkenum.cpp b/sources/shiboken6/libshiboken/sbkenum.cpp index 212fcec8..0013eb55 100644 --- a/sources/shiboken6/libshiboken/sbkenum.cpp +++ b/sources/shiboken6/libshiboken/sbkenum.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "sbkenum.h" +#include "sbkpep.h" #include "sbkstring.h" #include "helper.h" #include "sbkstaticstrings.h" @@ -67,7 +68,7 @@ PyTypeObject *getPyEnumMeta() return reinterpret_cast(PyEnumMeta); } } - Py_FatalError("Python module 'enum' not found"); + Py_FatalError("libshiboken: Python module 'enum' not found"); return nullptr; } @@ -77,7 +78,7 @@ void init_enum() if (isInitialized) return; if (!(isInitialized || _init_enum())) - Py_FatalError("could not init enum"); + Py_FatalError("libshiboken: could not init enum"); // PYSIDE-1735: Determine whether we should use the old or the new enum implementation. static PyObject *option = PySys_GetObject("pyside6_option_python_enum"); @@ -224,7 +225,7 @@ bool checkType(PyTypeObject *pyTypeObj) init_enum(); static PyTypeObject *meta = getPyEnumMeta(); - return Py_TYPE(pyTypeObj) == meta; + return Py_TYPE(reinterpret_cast(pyTypeObj)) == meta; } PyObject *getEnumItemFromValue(PyTypeObject *enumType, EnumValueType itemValue) @@ -282,14 +283,14 @@ void setTypeConverter(PyTypeObject *type, SbkConverter *converter, static void setModuleAndQualnameOnType(PyObject *type, const char *fullName) { - const char *colon = strchr(fullName, ':'); + const char *colon = std::strchr(fullName, ':'); assert(colon); int package_level = atoi(fullName); const char *mod = colon + 1; const char *qual = mod; for (int idx = package_level; idx > 0; --idx) { - const char *dot = strchr(qual, '.'); + const char *dot = std::strchr(qual, '.'); if (!dot) break; qual = dot + 1; @@ -306,7 +307,7 @@ static PyTypeObject *createEnumForPython(PyObject *scopeOrModule, const char *fullName, PyObject *pyEnumItems) { - const char *dot = strrchr(fullName, '.'); + const char *dot = std::strrchr(fullName, '.'); AutoDecRef name(Shiboken::String::fromCString(dot ? dot + 1 : fullName)); static PyObject *enumName = String::createStaticString("IntEnum"); @@ -473,7 +474,7 @@ PyTypeObject *createPythonEnum(const char *fullName, PyObject *pyEnumItems, return nullptr; } - const char *dot = strrchr(fullName, '.'); + const char *dot = std::strrchr(fullName, '.'); AutoDecRef name(Shiboken::String::fromCString(dot ? dot + 1 : fullName)); AutoDecRef callArgs(Py_BuildValue("(OO)", name.object(), pyEnumItems)); auto *newType = PyObject_Call(PyEnumType, callArgs, callDict); diff --git a/sources/shiboken6/libshiboken/sbkerrors.cpp b/sources/shiboken6/libshiboken/sbkerrors.cpp index 8dc3c639..33bcf69f 100644 --- a/sources/shiboken6/libshiboken/sbkerrors.cpp +++ b/sources/shiboken6/libshiboken/sbkerrors.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "sbkerrors.h" +#include "sbkpep.h" #include "sbkstring.h" #include "helper.h" #include "gilstate.h" @@ -129,13 +130,53 @@ static bool prependToExceptionMessage(PyObject *exc, const char *context) return true; } -struct ErrorStore { - PyObject *type; - PyObject *exc; - PyObject *traceback; +struct ErrorStore +{ + operator bool() const { return exc != nullptr; } + + PyObject *exc = nullptr; +#ifdef PEP_OLD_ERR_API + PyObject *traceback = nullptr; + PyObject *type = nullptr; +#endif }; -static thread_local ErrorStore savedError{}; +static void fetchError(ErrorStore &s) +{ +#ifdef PEP_OLD_ERR_API + PyErr_Fetch(&s.type, &s.exc, &s.traceback); +#else + s.exc = PyErr_GetRaisedException(); +#endif +} + +static void restoreError(ErrorStore &s) +{ +#ifdef PEP_OLD_ERR_API + PyErr_Restore(s.type, s.exc, s.traceback); + s.type = s.exc = s.traceback = nullptr; +#else + if (s.exc) { + PyErr_SetRaisedException(s.exc); + s.exc = nullptr; + } else { + PyErr_Clear(); + } +#endif +} + +static void releaseError(ErrorStore &s) +{ + Py_XDECREF(s.exc); + s.exc = nullptr; +#ifdef PEP_OLD_ERR_API + Py_XDECREF(s.type); + Py_XDECREF(s.traceback); + s.type = s.traceback = nullptr; +#endif +} + +static thread_local ErrorStore savedError; static bool hasPythonContext() { @@ -148,7 +189,7 @@ void storeErrorOrPrint() // Therefore, we handle the error when we are error checking, anyway. // But we do that only when we know that an error handler can pick it up. if (hasPythonContext()) - PyErr_Fetch(&savedError.type, &savedError.exc, &savedError.traceback); + fetchError(savedError); else PyErr_Print(); } @@ -158,7 +199,7 @@ void storeErrorOrPrint() static void storeErrorOrPrintWithContext(const char *context) { if (hasPythonContext()) { - PyErr_Fetch(&savedError.type, &savedError.exc, &savedError.traceback); + fetchError(savedError); prependToExceptionMessage(savedError.exc, context); } else { std::fputs(context, stderr); @@ -175,13 +216,42 @@ void storePythonOverrideErrorOrPrint(const char *className, const char *funcName PyObject *occurred() { - if (savedError.type) { - PyErr_Restore(savedError.type, savedError.exc, savedError.traceback); - savedError.type = nullptr; - } + if (savedError) + restoreError(savedError); return PyErr_Occurred(); } +Stash::Stash() : m_store(std::make_unique()) +{ + fetchError(*m_store); +} + +Stash::~Stash() +{ + restore(); +} + +PyObject *Stash::getException() const +{ + return m_store ? m_store->exc : nullptr; +} + +void Stash::restore() +{ + if (m_store) { + restoreError(*m_store); + m_store.reset(); + } +} + +void Stash::release() +{ + if (m_store) { + releaseError(*m_store); + m_store.reset(); + } +} + } // namespace Errors namespace Warnings diff --git a/sources/shiboken6/libshiboken/sbkerrors.h b/sources/shiboken6/libshiboken/sbkerrors.h index d7247ded..58576dc7 100644 --- a/sources/shiboken6/libshiboken/sbkerrors.h +++ b/sources/shiboken6/libshiboken/sbkerrors.h @@ -7,6 +7,8 @@ #include "sbkpython.h" #include "shibokenmacros.h" +#include + /// Craving for C++20 and std::source_location::current() #if defined(_MSC_VER) # define SBK_FUNC_INFO __FUNCSIG__ @@ -35,6 +37,32 @@ public: namespace Errors { +struct ErrorStore; + +/// Temporarily stash an error set in Python +class Stash +{ +public: + Stash(const Stash &) = delete; + Stash &operator=(const Stash &) = delete; + Stash(Stash &&) = delete; + Stash &operator=(Stash &&) = delete; + + LIBSHIBOKEN_API Stash(); + LIBSHIBOKEN_API ~Stash(); + + LIBSHIBOKEN_API operator bool() const { return getException() != nullptr; } + [[nodiscard]] LIBSHIBOKEN_API PyObject *getException() const; + + /// Restore the stored error + LIBSHIBOKEN_API void restore(); + /// Discard the stored error + LIBSHIBOKEN_API void release(); + +private: + std::unique_ptr m_store; +}; + LIBSHIBOKEN_API void setIndexOutOfBounds(Py_ssize_t value, Py_ssize_t minValue, Py_ssize_t maxValue); LIBSHIBOKEN_API void setInstantiateAbstractClass(const char *name); diff --git a/sources/shiboken6/libshiboken/sbkfeature_base.cpp b/sources/shiboken6/libshiboken/sbkfeature_base.cpp index b778367a..0b79ff3b 100644 --- a/sources/shiboken6/libshiboken/sbkfeature_base.cpp +++ b/sources/shiboken6/libshiboken/sbkfeature_base.cpp @@ -6,6 +6,7 @@ #include "autodecref.h" #include "pep384ext.h" #include "sbkenum.h" +#include "sbkerrors.h" #include "sbkstring.h" #include "sbkstaticstrings.h" #include "sbkstaticstrings_p.h" @@ -14,9 +15,28 @@ #include "gilstate.h" #include +#include +#include +#include using namespace Shiboken; +using StringViewVector = std::vector; + +static StringViewVector splitStringView(std::string_view s, char delimiter) +{ + StringViewVector result; + const auto size = s.size(); + for (std::string_view::size_type pos = 0; pos < size; ) { + auto nextPos = s.find(delimiter, pos); + if (nextPos == std::string_view::npos) + nextPos = size; + result.push_back(s.substr(pos, nextPos - pos)); + pos = nextPos + 1; + } + return result; +} + extern "C" { @@ -60,8 +80,8 @@ SelectableFeatureHook initSelectableFeature(SelectableFeatureHook func) void disassembleFrame(const char *marker) { Shiboken::GilState gil; - PyObject *error_type, *error_value, *error_traceback; - PyErr_Fetch(&error_type, &error_value, &error_traceback); + + Shiboken::Errors::Stash errorStash; static PyObject *dismodule = PyImport_ImportModule("dis"); static PyObject *disco = PyObject_GetAttrString(dismodule, "disco"); static PyObject *const _f_lasti = Shiboken::String::createStaticString("f_lasti"); @@ -84,12 +104,11 @@ void disassembleFrame(const char *marker) fprintf(stdout, "%s END line=%ld %s\n\n", marker, line, fname); } #if PY_VERSION_HEX >= 0x030C0000 && !Py_LIMITED_API - if (error_type) - PyErr_DisplayException(error_value); + if (auto *exc = errorStash.getException()) + PyErr_DisplayException(exc); #endif static PyObject *stdout_file = PySys_GetObject("stdout"); ignore.reset(PyObject_CallMethod(stdout_file, "flush", nullptr)); - PyErr_Restore(error_type, error_value, error_traceback); } // OpCodes: Adapt for each Python version by checking the defines in the generated header opcode_ids.h @@ -133,7 +152,6 @@ static int const CALL_METHOD = 161; static bool currentOpcode_Is_CallMethNoArgs() { static const auto number = _PepRuntimeVersion(); - static PyObject *flags = PySys_GetObject("flags"); // We look into the currently active operation if we are going to call // a method with zero arguments. auto *frame = PyEval_GetFrame(); @@ -200,27 +218,40 @@ static bool currentOpcode_Is_CallMethNoArgs() return opcode2 == CALL_OpCode(number) && oparg2 == 0; } +static inline PyObject *PyUnicode_fromStringView(std::string_view s) +{ + return PyUnicode_FromStringAndSize(s.data(), s.size()); +} + +static bool populateEnumDicts(PyObject *typeDict, PyObject *dict, const char *enumFlagInfo) +{ + StringViewVector parts = splitStringView(enumFlagInfo, ':'); + if (parts.size() < 2) + return false; + AutoDecRef name(PyUnicode_fromStringView(parts[0])); + AutoDecRef typeName(PyUnicode_fromStringView(parts[1])); + if (parts.size() >= 3) { + AutoDecRef key(PyUnicode_fromStringView(parts[2])); + auto *value = name.object(); + if (PyDict_SetItem(dict, key, value) != 0) + return false; + } + if (PyDict_SetItem(typeDict, name, typeName) != 0) + return false; + return true; +} + void initEnumFlagsDict(PyTypeObject *type) { // We create a dict for all flag enums that holds the original C++ name // and a dict that gives every enum/flag type name. - static PyObject *const split = Shiboken::String::createStaticString("split"); - static PyObject *const colon = Shiboken::String::createStaticString(":"); - auto sotp = PepType_SOTP(type); + auto *sotp = PepType_SOTP(type); auto **enumFlagInfo = sotp->enumFlagInfo; auto *dict = PyDict_New(); auto *typeDict = PyDict_New(); for (; *enumFlagInfo; ++enumFlagInfo) { - AutoDecRef line(PyUnicode_FromString(*enumFlagInfo)); - AutoDecRef parts(PyObject_CallMethodObjArgs(line, split, colon, nullptr)); - auto *name = PyList_GetItem(parts, 0); - if (PyList_Size(parts) == 3) { - auto *key = PyList_GetItem(parts, 2); - auto *value = name; - PyDict_SetItem(dict, key, value); - } - auto *typeName = PyList_GetItem(parts, 1); - PyDict_SetItem(typeDict, name, typeName); + if (!populateEnumDicts(typeDict, dict, *enumFlagInfo)) + std::cerr << __FUNCTION__ << ": Invalid enum \"" << *enumFlagInfo << '\n'; } sotp->enumFlagsDict = dict; sotp->enumTypeDict = typeDict; @@ -366,15 +397,11 @@ PyObject *mangled_type_getattro(PyTypeObject *type, PyObject *name) } if (!ret && name != ignAttr1 && name != ignAttr2) { - PyObject *error_type{}, *error_value{}, *error_traceback{}; - PyErr_Fetch(&error_type, &error_value, &error_traceback); + Shiboken::Errors::Stash errorsStash; ret = lookupUnqualifiedOrOldEnum(type, name); if (ret) { - Py_DECREF(error_type); - Py_XDECREF(error_value); - Py_XDECREF(error_traceback); - } else { - PyErr_Restore(error_type, error_value, error_traceback); + errorsStash.release(); + return ret; } } return ret; diff --git a/sources/shiboken6/libshiboken/sbkmodule.cpp b/sources/shiboken6/libshiboken/sbkmodule.cpp index 865978a1..c2d20098 100644 --- a/sources/shiboken6/libshiboken/sbkmodule.cpp +++ b/sources/shiboken6/libshiboken/sbkmodule.cpp @@ -8,6 +8,7 @@ #include "sbkstring.h" #include "sbkcppstring.h" #include "sbkconverter_p.h" +#include "sbkpep.h" #include #include @@ -116,7 +117,7 @@ static void incarnateSubtypes(PyObject *module, } } -static PyTypeObject *incarnateType(PyObject *module, const char *name, +static PyTypeObject *incarnateType(PyObject *module, const std::string &name, NameToTypeFunctionMap &nameToFunc) { // - locate the name and retrieve the generating function @@ -139,9 +140,8 @@ static PyTypeObject *incarnateType(PyObject *module, const char *name, initSelectableFeature(saveFeature); // - assign this object to the name in the module - auto *res = reinterpret_cast(type); - Py_INCREF(res); - PyModule_AddObject(module, name, res); // steals reference + Py_INCREF(reinterpret_cast(type)); + PepModule_AddType(module, type); // steals reference // - remove the entry, if not by something cleared. if (!nameToFunc.empty()) nameToFunc.erase(funcIter); @@ -152,7 +152,7 @@ static PyTypeObject *incarnateType(PyObject *module, const char *name, // PYSIDE-2404: Make sure that the mentioned classes really exist. // Used in `Pyside::typeName`. Because the result will be cached by // the creation of the type(s), this is efficient. -void loadLazyClassesWithName(const char *name) +void loadLazyClassesWithNameStd(const std::string &name) { for (auto const & tableIter : moduleToFuncs) { auto nameToFunc = tableIter.second; @@ -165,6 +165,11 @@ void loadLazyClassesWithName(const char *name) } } +void loadLazyClassesWithName(const char *name) +{ + loadLazyClassesWithNameStd(std::string(name)); +} + // PYSIDE-2404: Completely load all not yet loaded classes. // This is needed to resolve a star import. // PYSIDE-2898: Use a name list to pick the toplevel types. @@ -257,13 +262,6 @@ static PyMethodDef module_methods[] = { {nullptr, nullptr, 0, nullptr} }; -// Python 3.8 - 3.12 -static int const LOAD_CONST_312 = 100; -static int const IMPORT_NAME_312 = 108; -// Python 3.13 -static int const LOAD_CONST_313 = 83; -static int const IMPORT_NAME_313 = 75; - // OpCodes: Adapt for each Python version by checking the defines in the generated header opcode_ids.h // egrep '( LOAD_CONST | IMPORT_NAME )' opcode_ids.h @@ -339,9 +337,7 @@ static bool isImportStar(PyObject *module) } // PYSIDE-2404: These modules produce ambiguous names which we cannot handle, yet. -static std::unordered_set dontLazyLoad{ - "testbinding" -}; +static std::unordered_set dontLazyLoad; static const std::unordered_set knownModules{ "shiboken6.Shiboken", @@ -423,18 +419,19 @@ void AddTypeCreationFunction(PyObject *module, } void AddTypeCreationFunction(PyObject *module, - const char *containerName, + const char *enclosingName, TypeCreationFunction func, - const char *namePath) + const char *subTypeNamePath) { // - locate the module in the moduleTofuncs mapping auto tableIter = moduleToFuncs.find(module); assert(tableIter != moduleToFuncs.end()); // - Assign the name/generating function tcStruct. auto &nameToFunc = tableIter->second; - auto nit = nameToFunc.find(containerName); + auto nit = nameToFunc.find(enclosingName); // - insert namePath into the subtype vector of the main type. + std::string namePath(subTypeNamePath); nit->second.subtypeNames.emplace_back(namePath); // - insert it also as its own entry. nit = nameToFunc.find(namePath); @@ -490,21 +487,40 @@ static PyMethodDef lazy_methods[] = { {nullptr, nullptr, 0, nullptr} }; -PyObject *create(const char * /* modName */, void *moduleData) +PyObject *createOnly(const char * /* moduleName */, PyModuleDef *moduleData) + +{ + Shiboken::init(); + auto *module = PyModule_Create(moduleData); + if (module == nullptr) { + PyErr_Print(); + return nullptr; + } +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED); +#endif + return module; +} + +PyObject *create(const char *moduleName, PyModuleDef *moduleData) +{ + auto *module = createOnly(moduleName, moduleData); + if (module != nullptr) + exec(module); + return module; +} + +void exec(PyObject *module) { static auto *sysModules = PyImport_GetModuleDict(); - static auto *builtins = PyEval_GetBuiltins(); static auto *partial = Pep_GetPartialFunction(); static bool lazy_init{}; - Shiboken::init(); - auto *module = PyModule_Create(reinterpret_cast(moduleData)); - // Setup of a dir function for "missing" classes. auto *moduleDirTemplate = PyCFunction_NewEx(module_methods, nullptr, nullptr); // Turn this function into a bound object, so we have access to the module. auto *moduleDir = PyObject_CallFunctionObjArgs(partial, moduleDirTemplate, module, nullptr); - PyModule_AddObject(module, module_methods->ml_name, moduleDir); // steals reference + PepModule_Add(module, module_methods->ml_name, moduleDir); // steals reference // Insert an initial empty table for the module. NameToTypeFunctionMap empty; moduleToFuncs.insert(std::make_pair(module, empty)); @@ -518,10 +534,11 @@ PyObject *create(const char * /* modName */, void *moduleData) origModuleGetattro = PyModule_Type.tp_getattro; PyModule_Type.tp_getattro = PyModule_lazyGetAttro; // Add the lazy import redirection, keeping a reference. - origImportFunc = PyDict_GetItemString(builtins, "__import__"); + Shiboken::AutoDecRef builtins(PepEval_GetFrameBuiltins()); + origImportFunc = PyDict_GetItemString(builtins.object(), "__import__"); Py_INCREF(origImportFunc); AutoDecRef func(PyCFunction_NewEx(lazy_methods, nullptr, nullptr)); - PyDict_SetItemString(builtins, "__import__", func); + PyDict_SetItemString(builtins.object(), "__import__", func); lazy_init = true; } // PYSIDE-2404: Nuitka inserts some additional code in standalone mode @@ -529,11 +546,10 @@ PyObject *create(const char * /* modName */, void *moduleData) // that gets imported before the running import can call // `_PyImport_FixupExtensionObject` which does the insertion // into `sys.modules`. This can cause a race condition. - // Insert the module early into the module dict to prevend recursion. + // Insert the module early into the module dict to prevent recursion. PyDict_SetItemString(sysModules, PyModule_GetName(module), module); // Clear the non-existing name cache because we have a new module. Shiboken::Conversions::clearNegativeLazyCache(); - return module; } void registerTypes(PyObject *module, TypeInitStruct *types) @@ -577,7 +593,7 @@ bool replaceModuleDict(PyObject *module, PyObject *modClass, PyObject *dict) auto *modict = PyModule_GetDict(module); auto *modIntern = reinterpret_cast(module); if (modict != modIntern->md_dict) - Py_FatalError("The layout of modules is incompatible"); + Py_FatalError("libshiboken: The layout of modules is incompatible"); auto *hold = modIntern->md_dict; modIntern->md_dict = dict; Py_INCREF(dict); diff --git a/sources/shiboken6/libshiboken/sbkmodule.h b/sources/shiboken6/libshiboken/sbkmodule.h index ac77604b..c095a985 100644 --- a/sources/shiboken6/libshiboken/sbkmodule.h +++ b/sources/shiboken6/libshiboken/sbkmodule.h @@ -37,12 +37,19 @@ LIBSHIBOKEN_API void resolveLazyClasses(PyObject *module); LIBSHIBOKEN_API PyObject *import(const char *moduleName); /** - * Creates a new Python module named \p moduleName using the information passed in \p moduleData. - * In fact, \p moduleData expects a "PyMethodDef *" object, but that's for Python 2. A "void*" - * was preferred to make this work with future Python 3 support. + * Creates a new Python module named \p moduleName using the information passed in \p moduleData + * and calls exec() on it. * \returns a newly created module. */ -LIBSHIBOKEN_API PyObject *create(const char *moduleName, void *moduleData); +[[deprecated]] LIBSHIBOKEN_API PyObject *create(const char *moduleName, PyModuleDef *moduleData); + +/// Creates a new Python module named \p moduleName using the information passed in \p moduleData. +/// exec() is not called (Support for Nuitka). +/// \returns a newly created module. +LIBSHIBOKEN_API PyObject *createOnly(const char *moduleName, PyModuleDef *moduleData); + +/// Executes a module (multi-phase initialization helper) +LIBSHIBOKEN_API void exec(PyObject *module); using TypeCreationFunction = PyTypeObject *(*)(PyObject *module); @@ -52,9 +59,9 @@ LIBSHIBOKEN_API void AddTypeCreationFunction(PyObject *module, TypeCreationFunction func); LIBSHIBOKEN_API void AddTypeCreationFunction(PyObject *module, - const char *name, + const char *enclosingName, TypeCreationFunction func, - const char *containerName); + const char *subTypeNamePath); /** * Registers the list of types created by \p module. diff --git a/sources/shiboken6/libshiboken/sbkmodule_p.h b/sources/shiboken6/libshiboken/sbkmodule_p.h new file mode 100644 index 00000000..a9cfa8fb --- /dev/null +++ b/sources/shiboken6/libshiboken/sbkmodule_p.h @@ -0,0 +1,16 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef SBK_MODULE_P_H +#define SBK_MODULE_P_H + +#include + +namespace Shiboken::Module { + +/// PYSIDE-2404: Make sure that mentioned classes really exist. +void loadLazyClassesWithNameStd(const std::string &name); + +} // namespace Shiboken::Module + +#endif // SBK_MODULE_H diff --git a/sources/shiboken6/libshiboken/sbkpep.h b/sources/shiboken6/libshiboken/sbkpep.h new file mode 100644 index 00000000..5d07fbf3 --- /dev/null +++ b/sources/shiboken6/libshiboken/sbkpep.h @@ -0,0 +1,11 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef SBKPEP_H +#define SBKPEP_H + +#include "sbkversion.h" +#include "sbkpython.h" +#include "pep384impl.h" + +#endif // SBKPEP_H diff --git a/sources/shiboken6/libshiboken/sbkpepbuffer.h b/sources/shiboken6/libshiboken/sbkpepbuffer.h new file mode 100644 index 00000000..f92647c1 --- /dev/null +++ b/sources/shiboken6/libshiboken/sbkpepbuffer.h @@ -0,0 +1,32 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef SBKPEPBUFFER_H +#define SBKPEPBUFFER_H + +#include "bufferprocs_py37.h" + +// FIXME: Move back to sbktypefactory.h once Py_LIMITED_API >= 3.11 +extern "C" +{ +LIBSHIBOKEN_API PyTypeObject *SbkType_FromSpec_BMDWB(PyType_Spec *spec, + PyObject *bases, + PyTypeObject *meta, + int dictoffset, + int weaklistoffset, + PyBufferProcs *bufferprocs); +} // extern "C" + +// FIXME: Move back to helper.h once Py_LIMITED_API >= 3.11 +namespace Shiboken +{ +struct LIBSHIBOKEN_API debugPyBuffer +{ + explicit debugPyBuffer(const Py_buffer &b); + + const Py_buffer &m_buffer; +}; + +} // namespace Shiboken + +#endif // SBKBUFFER_H diff --git a/sources/shiboken6/libshiboken/sbkpython.h b/sources/shiboken6/libshiboken/sbkpython.h index 4914bec4..b001d273 100644 --- a/sources/shiboken6/libshiboken/sbkpython.h +++ b/sources/shiboken6/libshiboken/sbkpython.h @@ -4,8 +4,6 @@ #ifndef SBKPYTHON_H #define SBKPYTHON_H -#include "sbkversion.h" - // PYSIDE-2701: This definition is needed for all Python formats with "#". #define PY_SSIZE_T_CLEAN @@ -21,10 +19,6 @@ extern "C" { } # include -// Now we have the usual variables from Python.h . -# include "shibokenmacros.h" -// "pep384impl.h" may nowhere be included but in this file. -# include "pep384impl.h" # pragma pop_macro("slots") #else @@ -34,10 +28,6 @@ extern "C" { } # include -// Now we have the usual variables from Python.h . -# include "shibokenmacros.h" -// "pep384impl.h" may nowhere be included but in this file. -# include "pep384impl.h" #endif // In Python 3, Py_TPFLAGS_DEFAULT contains Py_TPFLAGS_HAVE_VERSION_TAG, diff --git a/sources/shiboken6/libshiboken/sbksmartpointer.cpp b/sources/shiboken6/libshiboken/sbksmartpointer.cpp index ee28f7db..b8cb84c7 100644 --- a/sources/shiboken6/libshiboken/sbksmartpointer.cpp +++ b/sources/shiboken6/libshiboken/sbksmartpointer.cpp @@ -3,6 +3,7 @@ #include "sbksmartpointer.h" #include "sbkstring.h" +#include "sbkpep.h" #include "autodecref.h" #include diff --git a/sources/shiboken6/libshiboken/sbkstaticstrings.cpp b/sources/shiboken6/libshiboken/sbkstaticstrings.cpp index 023de0ea..7d3890c3 100644 --- a/sources/shiboken6/libshiboken/sbkstaticstrings.cpp +++ b/sources/shiboken6/libshiboken/sbkstaticstrings.cpp @@ -69,6 +69,7 @@ STATIC_STRING_IMPL(rrshift, "__rrshift__") STATIC_STRING_IMPL(base, "__base__") STATIC_STRING_IMPL(bases, "__bases__") STATIC_STRING_IMPL(builtins, "__builtins__") +STATIC_STRING_IMPL(compiled, "__compiled__"); STATIC_STRING_IMPL(dictoffset, "__dictoffset__") STATIC_STRING_IMPL(func, "__func__") STATIC_STRING_IMPL(func_kind, "__func_kind__") diff --git a/sources/shiboken6/libshiboken/sbkstaticstrings_p.h b/sources/shiboken6/libshiboken/sbkstaticstrings_p.h index 2a337bf7..8813f4ae 100644 --- a/sources/shiboken6/libshiboken/sbkstaticstrings_p.h +++ b/sources/shiboken6/libshiboken/sbkstaticstrings_p.h @@ -23,7 +23,7 @@ namespace PyMagicName PyObject *base(); PyObject *bases(); PyObject *builtins(); -PyObject *code(); +PyObject *compiled(); PyObject *dictoffset(); PyObject *func_kind(); PyObject *iter(); diff --git a/sources/shiboken6/libshiboken/sbkstring.cpp b/sources/shiboken6/libshiboken/sbkstring.cpp index d2570f48..5ef6ebb7 100644 --- a/sources/shiboken6/libshiboken/sbkstring.cpp +++ b/sources/shiboken6/libshiboken/sbkstring.cpp @@ -2,10 +2,14 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "sbkstring.h" +#include "sbkpep.h" #include "sbkenum.h" #include "sbkstaticstrings_p.h" #include "autodecref.h" +#include +#include + namespace Shiboken::String { @@ -27,7 +31,7 @@ static PyObject *initPathLike() if (osmodule == nullptr || (PathLike = PyObject_GetAttrString(osmodule, "PathLike")) == nullptr) { PyErr_Print(); - Py_FatalError("cannot import os.PathLike"); + Py_FatalError("libshiboken: cannot import os.PathLike"); } return PathLike; } @@ -209,20 +213,20 @@ PyObject *getSnakeCaseName(const char *name, bool lower) * unchanged since that are the special OpenGL functions. */ if (!lower - || strlen(name) < 3 - || (name[0] == 'g' && name[1] == 'l' && isupper(name[2]))) + || std::strlen(name) < 3 + || (name[0] == 'g' && name[1] == 'l' && std::isupper(name[2]))) return createStaticString(name); char new_name[200 + 1] = {}; const char *p = name; char *q = new_name; for (; *p && q - new_name < 200; ++p, ++q) { - if (isupper(*p)) { - if (p != name && isupper(*(p - 1))) + if (std::isupper(*p)) { + if (p != name && std::isupper(*(p - 1))) return createStaticString(name); *q = '_'; ++q; - *q = tolower(*p); + *q = std::tolower(*p); } else { *q = *p; diff --git a/sources/shiboken6/libshiboken/sbktypefactory.cpp b/sources/shiboken6/libshiboken/sbktypefactory.cpp index f8d8b910..1a5796c8 100644 --- a/sources/shiboken6/libshiboken/sbktypefactory.cpp +++ b/sources/shiboken6/libshiboken/sbktypefactory.cpp @@ -2,7 +2,15 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "sbktypefactory.h" -#include "shiboken.h" +#include "autodecref.h" +#include "sbkpep.h" +#include "sbkpepbuffer.h" +#include "sbkstring.h" +#include "sbkstaticstrings.h" + +#include +#include +#include extern "C" { @@ -64,10 +72,18 @@ static PyObject *_PyType_FromSpecWithBasesHack(PyType_Spec *spec, } for (Py_ssize_t idx = 0, n = PyTuple_Size(bases); idx < n; ++idx) { - PyTypeObject *base = reinterpret_cast(PyTuple_GetItem(bases, idx)); - PyTypeObject *meta = Py_TYPE(base); + PyObject *obBase = PyTuple_GetItem(bases, idx); + auto *base = reinterpret_cast(obBase); + PyTypeObject *meta = Py_TYPE(obBase); if (meta->tp_new != PyType_Type.tp_new) { // make sure there is no second meta class + if (keepMeta != nullptr) { + std::cerr << "Warning: " << __FUNCTION__ + << ": multiple meta classes found for " << spec->name << " at " + << idx << ": " << PepType_GetFullyQualifiedNameStr(base) + << " in addition to " + << PepType_GetFullyQualifiedNameStr(keepMeta) << '\n'; + } assert(keepMeta == nullptr); keepMeta = meta; keepNew = meta->tp_new; @@ -111,7 +127,7 @@ PyTypeObject *SbkType_FromSpec_BMDWB(PyType_Spec *spec, // __name__ : "subclass" PyType_Spec new_spec = *spec; - const char *colon = strchr(spec->name, ':'); + const char *colon = std::strchr(spec->name, ':'); assert(colon); int package_level = atoi(spec->name); const char *mod = new_spec.name = colon + 1; @@ -122,7 +138,7 @@ PyTypeObject *SbkType_FromSpec_BMDWB(PyType_Spec *spec, const char *qual = mod; for (int idx = package_level; idx > 0; --idx) { - const char *dot = strchr(qual, '.'); + const char *dot = std::strchr(qual, '.'); if (!dot) break; qual = dot + 1; @@ -134,11 +150,10 @@ PyTypeObject *SbkType_FromSpec_BMDWB(PyType_Spec *spec, auto *type = reinterpret_cast(obType); if (meta) { - PyTypeObject *hold = Py_TYPE(type); - obType->ob_type = meta; - Py_INCREF(Py_TYPE(type)); + PyTypeObject *hold = std::exchange(obType->ob_type, meta); + Py_INCREF(reinterpret_cast(Py_TYPE(obType))); if (hold->tp_flags & Py_TPFLAGS_HEAPTYPE) - Py_DECREF(hold); + Py_DECREF(reinterpret_cast(hold)); } if (dictoffset) @@ -296,7 +311,7 @@ _PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases) } /* Set the type name and qualname */ - s = strrchr(const_cast(spec->name), '.'); + s = std::strrchr(const_cast(spec->name), '.'); if (s == nullptr) s = (char*)spec->name; else @@ -364,7 +379,7 @@ _PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases) if (slot->slot == Py_tp_doc) { const char *old_doc = reinterpret_cast(slot->pfunc); //_PyType_DocWithoutSignature(type->tp_name, slot->pfunc); - size_t len = strlen(old_doc)+1; + size_t len = std::strlen(old_doc)+1; char *tp_doc = reinterpret_cast(PyObject_MALLOC(len)); if (tp_doc == nullptr) { type->tp_doc = nullptr; diff --git a/sources/shiboken6/libshiboken/sbktypefactory.h b/sources/shiboken6/libshiboken/sbktypefactory.h index 81cb32d4..ad3d3c26 100644 --- a/sources/shiboken6/libshiboken/sbktypefactory.h +++ b/sources/shiboken6/libshiboken/sbktypefactory.h @@ -4,7 +4,8 @@ #ifndef SBKTYPEFACTORY_H #define SBKTYPEFACTORY_H -#include "sbkpython.h" +#include "sbkpepbuffer.h" +#include "shibokenmacros.h" extern "C" { @@ -14,12 +15,6 @@ LIBSHIBOKEN_API PyTypeObject *SbkType_FromSpec(PyType_Spec *); LIBSHIBOKEN_API PyTypeObject *SbkType_FromSpecWithMeta(PyType_Spec *, PyTypeObject *); LIBSHIBOKEN_API PyTypeObject *SbkType_FromSpecWithBases(PyType_Spec *, PyObject *); LIBSHIBOKEN_API PyTypeObject *SbkType_FromSpecBasesMeta(PyType_Spec *, PyObject *, PyTypeObject *); -LIBSHIBOKEN_API PyTypeObject *SbkType_FromSpec_BMDWB(PyType_Spec *spec, - PyObject *bases, - PyTypeObject *meta, - int dictoffset, - int weaklistoffset, - PyBufferProcs *bufferprocs); } //extern "C" diff --git a/sources/shiboken6/libshiboken/shibokenbuffer.cpp b/sources/shiboken6/libshiboken/shibokenbuffer.cpp index d0461389..f701506a 100644 --- a/sources/shiboken6/libshiboken/shibokenbuffer.cpp +++ b/sources/shiboken6/libshiboken/shibokenbuffer.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "shibokenbuffer.h" +#include "sbkpepbuffer.h" #include #include diff --git a/sources/shiboken6/libshiboken/signature/signature.cpp b/sources/shiboken6/libshiboken/signature/signature.cpp index c3dee51a..593ac504 100644 --- a/sources/shiboken6/libshiboken/signature/signature.cpp +++ b/sources/shiboken6/libshiboken/signature/signature.cpp @@ -18,6 +18,7 @@ #include "basewrapper.h" #include "autodecref.h" +#include "sbkpep.h" #include "sbkstring.h" #include "sbkstaticstrings.h" #include "sbkstaticstrings_p.h" @@ -26,6 +27,7 @@ #include #include +#include using namespace Shiboken; @@ -68,7 +70,7 @@ PyObject *GetClassOrModOf(PyObject *ob) return _get_class_of_descr(ob); if (Py_TYPE(ob) == &PyWrapperDescr_Type) return _get_class_of_descr(ob); - Py_FatalError("unexpected type in GetClassOrModOf"); + Py_FatalError("libshiboken: unexpected type in GetClassOrModOf"); return nullptr; } @@ -89,7 +91,7 @@ PyObject *GetTypeKey(PyObject *ob) } AutoDecRef class_name(PyObject_GetAttr(ob, PyMagicName::qualname())); if (class_name.isNull()) { - Py_FatalError("Signature: missing class name in GetTypeKey"); + Py_FatalError("libshiboken: missing class name in GetTypeKey"); return nullptr; } return Py_BuildValue("(OO)", module_name.object(), class_name.object()); @@ -302,32 +304,35 @@ static PyObject *feature_import(PyObject * /* self */, PyObject *args, PyObject return ret; // feature_import did not handle it, so call the normal import. Py_DECREF(ret); - static PyObject *builtins = PyEval_GetBuiltins(); - PyObject *origImportFunc = PyDict_GetItemString(builtins, "__orig_import__"); + Shiboken::AutoDecRef builtins(PepEval_GetFrameBuiltins()); + PyObject *origImportFunc = PyDict_GetItemString(builtins.object(), "__orig_import__"); if (origImportFunc == nullptr) { - Py_FatalError("builtins has no \"__orig_import__\" function"); + Py_FatalError("libshiboken: builtins has no \"__orig_import__\" function"); } - // PYSIDE-3054: Instead of just calling the original import, we temporarily - // reset the whole import function to the previous version. - // This prevents unforeseen recursions like in settrace. - PyObject *featureImportFunc = PyDict_GetItemString(builtins, "__import__"); - Py_INCREF(origImportFunc); - Py_INCREF(featureImportFunc); - PyDict_SetItemString(builtins, "__import__", origImportFunc); ret = PyObject_Call(origImportFunc, args, kwds); if (ret) { + // PYSIDE-3054: Instead of just calling the original import, we temporarily + // reset the whole import function to the previous version. + // This prevents unforeseen recursions like in settrace. + PyObject *featureImportFunc = PyDict_GetItemString(builtins.object(), "__import__"); + Py_INCREF(origImportFunc); + Py_INCREF(featureImportFunc); + PyDict_SetItemString(builtins.object(), "__import__", origImportFunc); + // PYSIDE-2029: Intercept after the import to search for PySide usage. PyObject *post = PyObject_CallFunctionObjArgs(pyside_globals->feature_imported_func, ret, nullptr); Py_XDECREF(post); + + PyDict_SetItemString(builtins.object(), "__import__", featureImportFunc); + Py_DECREF(origImportFunc); + Py_DECREF(featureImportFunc); + if (post == nullptr) { Py_DECREF(ret); ret = nullptr; } } - PyDict_SetItemString(builtins, "__import__", featureImportFunc); - Py_DECREF(origImportFunc); - Py_DECREF(featureImportFunc); return ret; } @@ -535,7 +540,7 @@ static int _finishSignaturesCommon(PyObject *module) // the shiboken module (or a test module). [[maybe_unused]] const char *name = PyModule_GetName(module); if (pyside_globals->finish_import_func == nullptr) { - assert(strncmp(name, "PySide6.", 8) != 0); + assert(std::strncmp(name, "PySide6.", 8) != 0); return 0; } // Call a Python function which has to finish something as well. @@ -685,8 +690,8 @@ static PyObject *adjustFuncName(const char *func_name) static PyObject *ns = PyModule_GetDict(mapping); char _path[200 + 1] = {}; - const char *_name = strrchr(func_name, '.'); - strncat(_path, func_name, _name - func_name); + const char *_name = std::strrchr(func_name, '.'); + std::strncat(_path, func_name, _name - func_name); ++_name; // This is a very cheap call into `mapping.py`. @@ -768,7 +773,7 @@ void SetError_Argument(PyObject *args, const char *func_name, PyObject *info) AutoDecRef new_func_name(adjustFuncName(func_name)); if (new_func_name.isNull()) { PyErr_Print(); - Py_FatalError("seterror_argument failed to call update_mapping"); + Py_FatalError("libshiboken: seterror_argument failed to call update_mapping"); } if (info == nullptr) info = Py_None; @@ -776,13 +781,13 @@ void SetError_Argument(PyObject *args, const char *func_name, PyObject *info) args, new_func_name.object(), info, nullptr)); if (res.isNull()) { PyErr_Print(); - Py_FatalError("seterror_argument did not receive a result"); + Py_FatalError("libshiboken: seterror_argument did not receive a result"); } PyObject *err{}; PyObject *msg{}; if (!PyArg_UnpackTuple(res, func_name, 2, 2, &err, &msg)) { PyErr_Print(); - Py_FatalError("unexpected failure in seterror_argument"); + Py_FatalError("libshiboken: unexpected failure in seterror_argument"); } PyErr_SetObject(err, msg); } diff --git a/sources/shiboken6/libshiboken/signature/signature_extend.cpp b/sources/shiboken6/libshiboken/signature/signature_extend.cpp index 3ce9e21e..b9ce2944 100644 --- a/sources/shiboken6/libshiboken/signature/signature_extend.cpp +++ b/sources/shiboken6/libshiboken/signature/signature_extend.cpp @@ -22,12 +22,15 @@ // Shiboken.ObjectType and Shiboken.EnumMeta have new getsets, instead. #include "autodecref.h" +#include "sbkpep.h" #include "sbkstring.h" #include "sbkstaticstrings.h" #include "sbkstaticstrings_p.h" #include "signature_p.h" +#include + using namespace Shiboken; extern "C" { @@ -79,7 +82,7 @@ PyObject *pyside_md_get___signature__(PyObject *ob_md, PyObject *modifier) if (func.object() == Py_None) Py_RETURN_NONE; if (func.isNull()) - Py_FatalError("missing mapping in MethodDescriptor"); + Py_FatalError("libshiboken: missing mapping in MethodDescriptor"); return pyside_cf_get___signature__(func, modifier); } @@ -123,11 +126,11 @@ static PyObject *handle_doc(PyObject *ob, PyObject *old_descr) bool isModule = PyModule_Check(ob_type_mod.object()); const char *name = isModule ? PyModule_GetName(ob_type_mod.object()) - : reinterpret_cast(ob_type_mod.object())->tp_name; + : PepType_GetFullyQualifiedNameStr(reinterpret_cast(ob_type_mod.object())); PyObject *res{}; if (handle_doc_in_progress || name == nullptr - || (isModule && strncmp(name, "PySide6.", 8) != 0)) { + || (isModule && std::strncmp(name, "PySide6.", 8) != 0)) { res = PyObject_CallMethodObjArgs(old_descr, PyMagicName::get(), ob, nullptr); } else { handle_doc_in_progress++; diff --git a/sources/shiboken6/libshiboken/signature/signature_globals.cpp b/sources/shiboken6/libshiboken/signature/signature_globals.cpp index 0a08309c..b6270804 100644 --- a/sources/shiboken6/libshiboken/signature/signature_globals.cpp +++ b/sources/shiboken6/libshiboken/signature/signature_globals.cpp @@ -9,6 +9,7 @@ // #include "autodecref.h" +#include "sbkpep.h" #include "sbkstring.h" #include "sbkstaticstrings.h" #include "sbkstaticstrings_p.h" @@ -16,6 +17,8 @@ #include "signature_p.h" +#include + using namespace Shiboken; extern "C" { @@ -54,9 +57,12 @@ static safe_globals_struc *init_phase_1() AutoDecRef bytes(PyBytes_FromStringAndSize(bytes_cast, sizeof(PySide_SignatureLoader))); if (bytes.isNull()) break; + + AutoDecRef builtins; #if defined(Py_LIMITED_API) || defined(SHIBOKEN_NO_EMBEDDING_PYC) - PyObject *builtins = PyEval_GetBuiltins(); - PyObject *compile = PyDict_GetItem(builtins, PyName::compile()); + builtins.reset(PepEval_GetFrameBuiltins()); + PyObject *compile = PyDict_GetItem(builtins.object(), PyName::compile()); + builtins.reset(nullptr); if (compile == nullptr) break; AutoDecRef code_obj(PyObject_CallFunction(compile, "Oss", @@ -72,8 +78,10 @@ static safe_globals_struc *init_phase_1() break; // Initialize the module PyObject *mdict = PyModule_GetDict(p->helper_module); - if (PyDict_SetItem(mdict, PyMagicName::builtins(), PyEval_GetBuiltins()) < 0) + builtins.reset(PepEval_GetFrameBuiltins()); + if (PyDict_SetItem(mdict, PyMagicName::builtins(), builtins.object()) < 0) break; + builtins.reset(nullptr); /********************************************************************* * @@ -123,7 +131,7 @@ static safe_globals_struc *init_phase_1() } while (false); PyErr_Print(); - Py_FatalError("could not initialize part 1"); + Py_FatalError("libshiboken/signature: could not initialize part 1"); return nullptr; } @@ -139,8 +147,9 @@ static int init_phase_2(safe_globals_struc *p, PyMethodDef *methods) Py_DECREF(v); } // The first entry is __feature_import__, add documentation. - PyObject *builtins = PyEval_GetBuiltins(); - PyObject *imp_func = PyDict_GetItemString(builtins, "__import__"); + Shiboken::AutoDecRef builtins(PepEval_GetFrameBuiltins()); + PyObject *imp_func = PyDict_GetItemString(builtins.object(), "__import__"); + builtins.reset(nullptr); PyObject *imp_doc = PyObject_GetAttrString(imp_func, "__doc__"); signature_methods[0].ml_doc = String::toCString(imp_doc); @@ -200,7 +209,7 @@ static int init_phase_2(safe_globals_struc *p, PyMethodDef *methods) } while (0); PyErr_Print(); - Py_FatalError("could not initialize part 2"); + Py_FatalError("libshiboken/signature: could not initialize part 2"); return -1; } @@ -247,7 +256,7 @@ void init_shibokensupport_module(void) #ifndef _WIN32 // We enable the stack trace in CI, only. const char *testEnv = getenv("QTEST_ENVIRONMENT"); - if (testEnv && strstr(testEnv, "ci")) + if (testEnv && std::strstr(testEnv, "ci")) signal(SIGSEGV, handler); // install our handler #endif // _WIN32 diff --git a/sources/shiboken6/libshiboken/signature/signature_helper.cpp b/sources/shiboken6/libshiboken/signature/signature_helper.cpp index 3ecddbf0..0b9c5cba 100644 --- a/sources/shiboken6/libshiboken/signature/signature_helper.cpp +++ b/sources/shiboken6/libshiboken/signature/signature_helper.cpp @@ -11,6 +11,7 @@ // #include "autodecref.h" +#include "sbkpep.h" #include "sbkstring.h" #include "sbkstaticstrings.h" #include "sbkstaticstrings_p.h" @@ -32,7 +33,7 @@ static int _fixup_getset(PyTypeObject *type, const char *name, PyGetSetDef *new_ PyGetSetDef *gsp = type->tp_getset; if (gsp != nullptr) { for (; gsp->name != nullptr; gsp++) { - if (strcmp(gsp->name, name) == 0) { + if (std::strcmp(gsp->name, name) == 0) { new_gsp->set = gsp->set; new_gsp->doc = gsp->doc; new_gsp->closure = gsp->closure; @@ -43,7 +44,7 @@ static int _fixup_getset(PyTypeObject *type, const char *name, PyGetSetDef *new_ PyMemberDef *md = type->tp_members; if (md != nullptr) for (; md->name != nullptr; md++) - if (strcmp(md->name, name) == 0) + if (std::strcmp(md->name, name) == 0) return 1; return 0; } @@ -64,7 +65,7 @@ int add_more_getsets(PyTypeObject *type, PyGetSetDef *gsp, PyObject **doc_descr) PyObject *have_descr = PyDict_GetItemString(dict, gsp->name); if (have_descr != nullptr) { Py_INCREF(have_descr); - if (strcmp(gsp->name, "__doc__") == 0) + if (std::strcmp(gsp->name, "__doc__") == 0) *doc_descr = have_descr; else assert(false); @@ -93,7 +94,7 @@ static PyObject *get_funcname(PyObject *ob) PyObject *func_name = PyObject_GetAttr(func, PyMagicName::name()); Py_DECREF(func); if (func_name == nullptr) - Py_FatalError("unexpected name problem in compute_name_key"); + Py_FatalError("libshiboken: unexpected name problem in compute_name_key"); return func_name; } @@ -321,7 +322,7 @@ int _build_func_to_type(PyObject *obtype) // PYSIDE-2404: Get the original dict for late initialization. // The dict might have been switched before signature init. - static const auto *pyTypeType_tp_dict = PepType_GetDict(&PyType_Type); + static auto *pyTypeType_tp_dict = PepType_GetDict(&PyType_Type); if (Py_TYPE(dict) != Py_TYPE(pyTypeType_tp_dict)) { tpDict.reset(PyObject_GetAttr(dict, PyName::orig_dict())); dict = tpDict.object(); diff --git a/sources/shiboken6/libshiboken/voidptr.cpp b/sources/shiboken6/libshiboken/voidptr.cpp index ce85d494..5f251afa 100644 --- a/sources/shiboken6/libshiboken/voidptr.cpp +++ b/sources/shiboken6/libshiboken/voidptr.cpp @@ -187,11 +187,9 @@ static const char falseString[] = "False" ; PyObject *SbkVoidPtrObject_repr(PyObject *v) { - - auto *sbkObject = reinterpret_cast(v); PyObject *s = PyUnicode_FromFormat("%s(%p, %zd, %s)", - Py_TYPE(sbkObject)->tp_name, + Py_TYPE(v)->tp_name, sbkObject->cptr, sbkObject->size, sbkObject->isWritable ? trueString : falseString); @@ -203,7 +201,7 @@ PyObject *SbkVoidPtrObject_str(PyObject *v) { auto *sbkObject = reinterpret_cast(v); PyObject *s = PyUnicode_FromFormat("%s(Address %p, Size %zd, isWritable %s)", - Py_TYPE(sbkObject)->tp_name, + Py_TYPE(v)->tp_name, sbkObject->cptr, sbkObject->size, sbkObject->isWritable ? trueString : falseString); @@ -299,7 +297,7 @@ static int voidPointerInitialized = false; void init() { if (PyType_Ready(SbkVoidPtr_TypeF()) < 0) - Py_FatalError("[libshiboken] Failed to initialize Shiboken.VoidPtr type."); + Py_FatalError("libshiboken: Failed to initialize Shiboken.VoidPtr type."); else voidPointerInitialized = true; } @@ -307,9 +305,10 @@ void init() void addVoidPtrToModule(PyObject *module) { if (voidPointerInitialized) { - Py_INCREF(SbkVoidPtr_TypeF()); - PyModule_AddObject(module, PepType_GetNameStr(SbkVoidPtr_TypeF()), - reinterpret_cast(SbkVoidPtr_TypeF())); + auto *type = SbkVoidPtr_TypeF(); + auto *obType = reinterpret_cast(type); + Py_INCREF(obType); + PepModule_AddType(module, type); } } diff --git a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/layout.py b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/layout.py index 8eea431c..98335a98 100644 --- a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/layout.py +++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/layout.py @@ -372,6 +372,14 @@ def create_signature_union(props, key): param = inspect.Parameter(name, kind, annotation=ann, default=default) params.append(param) + # Find the index of variadic positional parameter, if any + # And update the parameter kind that comes after + idx = next((i for i, p in enumerate(params) if p.kind == _VAR_POSITIONAL), None) + if idx is not None: + for i, p in enumerate(params): + if i > idx and p.kind != _VAR_KEYWORD: + params[i] = p.replace(kind=_KEYWORD_ONLY) + ret_anno = annotations.get('return', _empty) if ret_anno is not _empty and props["fullname"] in missing_optional_return: ret_anno = typing.Union[ret_anno] diff --git a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/enum_sig.py b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/enum_sig.py index 6f86df8c..b31b161a 100644 --- a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/enum_sig.py +++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/enum_sig.py @@ -53,6 +53,12 @@ def is_inconsistent_overload(signatures): return count != 0 and count != len(signatures) +def is_relevant_type(thing): + t = str(type(thing)) + return (("PySide" in t or "getset_descriptor" in t) + and "QMetaObject" not in t) + + class ExactEnumerator: """ ExactEnumerator enumerates all signatures in a module as they are. @@ -178,7 +184,9 @@ class ExactEnumerator: # Support attributes that have PySide types as values, # but we skip the 'staticMetaObject' that needs # to be defined at a QObject level. - elif "PySide" in str(type(thing)) and "QMetaObject" not in str(type(thing)): + # PYSIDE-3034: added public variables, extracted helper function to + # avoid repetitive calls of str(type(thing)) + elif is_relevant_type(thing): if class_name not in attributes: attributes[class_name] = {} attributes[class_name][thing_name] = thing diff --git a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py index c5dc4464..cca7ae74 100644 --- a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py +++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py @@ -1,7 +1,9 @@ LICENSE_TEXT = """ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -from __future__ import annotations +""" +MYPY_TEXT = """ +# mypy: disable-error-code="override, overload-overlap" """ # flake8: noqa E:402 @@ -30,6 +32,38 @@ from shibokensupport.signature.lib.tool import build_brace_pattern indent = " " * 4 +TYPE_MAP = { + # Qt integer types + "qint64": "int", + "qint32": "int", + "qint16": "int", + "qsizetype": "int", + "quint32": "int", + "quint64": "int", + "size_t": "int", + "uint": "int", + "ushort": "int", + "ulong": "int", + "unsigned char": "int", + "unsigned int": "int", + + # Qt floating types + "qreal": "float", + + # Qt string-like + "QString": "str", + "QStringList": "typing.List[str]", + "QChar": "str", + + # Qt containers (minimal) + "QList": "typing.List", + "QVariant": "typing.Any", + + # C strings + "char*": "str", + "const char*": "str", +} + class Writer: def __init__(self, outfile, *args): @@ -69,12 +103,12 @@ class Formatter(Writer): backup = inspect.formatannotation @classmethod - def formatannotation(cls, annotation, base_module=None): + def formatannotation(cls, annotation, base_module=None, *args, **kwargs): if getattr(annotation, '__module__', None) == 'typing': # do not remove the prefix! return repr(annotation) # do the normal action. - return cls.backup(annotation, base_module) + return cls.backup(annotation, base_module, *args, **kwargs) @classmethod def fix_typing_prefix(cls, signature): @@ -84,6 +118,29 @@ class Formatter(Writer): inspect.formatannotation = cls.backup return stringized + @classmethod + def normalize_type(cls, type_repr: str) -> str: + if not type_repr: + return "typing.Any" + if type_repr in {"void", "void*"}: + return "typing.Any" + if any(x in type_repr for x in ("QRhi", ".ComponentType", ".Semantic")): + return "int" + if ( " " in type_repr and + not any(x in type_repr for x in ("*", "::", "<", ">", "[", "]"))): + return "typing.Any" + if type_repr.startswith("QList["): + inner = type_repr[len("QList["):-1] + inner = cls.normalize_type(inner) + return f"typing.List[{inner}]" + if type_repr.startswith("QMap[") or type_repr.startswith("QHash["): + inner = type_repr[type_repr.find("[") + 1:-1] + key, value = map(str.strip, inner.split(",", 1)) + key = cls.normalize_type(key) + value = cls.normalize_type(value) + return f"typing.Dict[{key}, {value}]" + return TYPE_MAP.get(type_repr, type_repr) + # Adding a pattern to substitute "Union[T, NoneType]" by "Optional[T]" # I tried hard to replace typing.Optional by a simple override, but # this became _way_ too much. @@ -214,7 +271,7 @@ class Formatter(Writer): def enum(self, class_name, enum_name, value): spaces = indent * self.level hexval = hex(value) - self.print(f"{spaces}{enum_name:25} = ... # {hexval if value >= 0 else value}") + self.print(f"{spaces}{enum_name:25} = {hexval if value >= 0 else value}") yield @contextmanager @@ -222,7 +279,12 @@ class Formatter(Writer): spaces = indent * self.level # PYSIDE-2903: Use a fully qualified name in the type comment. full_name = f"{type(attr_value).__module__}.{type(attr_value).__qualname__}" - self.print(f"{spaces}{attr_name:25} = ... # type: {full_name}") + if full_name == "builtins.getset_descriptor": + # PYSIDE-3034: Public variable types added to __doc__ + type_repr = self.normalize_type(attr_value.__doc__) + else: + type_repr = full_name + self.print(f"{spaces}{attr_name:25} = ... # type: {type_repr}") yield @contextmanager @@ -311,10 +373,9 @@ def generate_pyi(import_name, outpath, options): """ This file contains the exact signatures for all functions in module {import_name}, except for defaults which are replaced by "...". - - # mypy: disable-error-code="override, overload-overlap" """ ''')) + fmt.print(MYPY_TEXT.strip()) HintingEnumerator(fmt).module(import_name) fmt.print("# eof") # Postprocess: resolve the imports diff --git a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/mapping.py b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/mapping.py index 4c20a111..9d1c3fd5 100644 --- a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/mapping.py +++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/mapping.py @@ -266,6 +266,7 @@ type_map.update({ "QSet": typing.Set, "QString": str, "QLatin1String": str, + "QAnyStringView": str, "QStringView": str, "QStringList": StringList, "quint16": int, @@ -514,6 +515,7 @@ def init_PySide6_QtCore(): "PySide6.QtCore.QUrl.ComponentFormattingOptions": PySide6.QtCore.QUrl.ComponentFormattingOption, # mismatch option/enum, why??? "PyUnicode": typing.Text, + "QByteArray": typing.Union[PySide6.QtCore.QByteArray, bytes, bytearray, memoryview], "QByteArrayView": PySide6.QtCore.QByteArray, "Q_NULLPTR": None, "QCalendar.Unspecified": PySide6.QtCore.QCalendar.Unspecified, @@ -541,6 +543,8 @@ def init_PySide6_QtCore(): "QVariant.Type": type, # not so sure here... "QVariantMap": typing.Dict[str, Variant], "std.chrono.seconds{5}" : ellipsis, + "Internal.defaultTryTimeout": 5000, + "static_cast(Internal.defaultTryTimeout.count())": 5000 }) from shibokensupport.signature.parser import using_snake_case if using_snake_case(): @@ -556,6 +560,9 @@ def init_PySide6_QtCore(): type_map_tuple.update({("PySide6.QtCore.QObject.disconnect", "char*"): str}) type_map_tuple.update({("PySide6.QtCore.QObject.receivers", "char*"): str}) type_map_tuple.update({("PySide6.QtCore.qtTrId", "char*"): str}) + # special case - char default is 'int'. + # Here we manually set it to map to 'str'. + type_map_tuple.update({("PySide6.QtCore.QLocale.toString", "char"): str}) return locals() @@ -620,7 +627,7 @@ def init_PySide6_QtWidgets(): def init_PySide6_QtSql(): from PySide6.QtSql import QSqlDatabase type_map.update({ - "QLatin1StringView(QSqlDatabase.defaultConnection)": QSqlDatabase.defaultConnection, + "QSqlDatabase.defaultConnectionName()": "", "QVariant.Invalid": Invalid("Variant"), # not sure what I should create, here... }) return locals() @@ -736,6 +743,9 @@ def init_testbinding(): # Functions which should return Optional(result) but don't. missing_optional_return = { + "PySide6.QtCore.QObject.parent", + "PySide6.QtGui.QGuiApplication.modalWindow", + "PySide6.QtGui.QGuiApplication.screenAt", "PySide6.QtWidgets.QApplication.activeModalWidget", "PySide6.QtWidgets.QApplication.activePopupWidget", "PySide6.QtWidgets.QApplication.activeWindow", @@ -743,16 +753,41 @@ missing_optional_return = { "PySide6.QtWidgets.QApplication.setStyle", "PySide6.QtWidgets.QApplication.topLevelAt", "PySide6.QtWidgets.QApplication.widgetAt", + "PySide6.QtWidgets.QBoxLayout.itemAt", + "PySide6.QtWidgets.QBoxLayout.takeAt", + "PySide6.QtWidgets.QButtonGroup.checkedButton", "PySide6.QtWidgets.QComboBox.completer", "PySide6.QtWidgets.QComboBox.lineEdit", "PySide6.QtWidgets.QComboBox.validator", + "PySide6.QtWidgets.QCompleter.popup", + "PySide6.QtWidgets.QFormLayout.itemAt", + "PySide6.QtWidgets.QFormLayout.takeAt", + "PySide6.QtWidgets.QGraphicsAnchorLayout.itemAt", + "PySide6.QtWidgets.QGraphicsGridLayout.itemAt", + "PySide6.QtWidgets.QGraphicsLayout.itemAt", + "PySide6.QtWidgets.QGraphicsLinearLayout.itemAt", + "PySide6.QtWidgets.QGraphicsScene.itemAt", + "PySide6.QtWidgets.QGraphicsView.itemAt", "PySide6.QtWidgets.QGridLayout.itemAt", "PySide6.QtWidgets.QGridLayout.itemAtPosition", + "PySide6.QtWidgets.QGridLayout.takeAt", "PySide6.QtWidgets.QLayout.itemAt", + "PySide6.QtWidgets.QLayout.replaceWidget", + "PySide6.QtWidgets.QLayout.takeAt", + "PySide6.QtWidgets.QListWidget.itemAt", + "PySide6.QtWidgets.QScrollArea.widget", + "PySide6.QtWidgets.QSplitter.widget", + "PySide6.QtWidgets.QStackedLayout.itemAt", + "PySide6.QtWidgets.QStackedLayout.takeAt", + "PySide6.QtWidgets.QStackedLayout.widget", + "PySide6.QtWidgets.QStackedWidget.widget", + "PySide6.QtWidgets.QTabWidget.widget", "PySide6.QtWidgets.QTableWidget.horizontalHeaderItem", "PySide6.QtWidgets.QTableWidget.item", "PySide6.QtWidgets.QTableWidget.itemAt", "PySide6.QtWidgets.QTableWidget.mimeData", + "PySide6.QtWidgets.QToolBox.widget", + "PySide6.QtWidgets.QTreeWidget.itemAt", "PySide6.QtWidgets.QTreeWidget.takeTopLevelItem", "PySide6.QtWidgets.QTreeWidget.topLevelItem", "PySide6.QtWidgets.QWidget.childAt", diff --git a/sources/shiboken6/shibokenmodule/shibokenmodule.cpp b/sources/shiboken6/shibokenmodule/shibokenmodule.cpp index 7eb7e37f..f07eaf0b 100644 --- a/sources/shiboken6/shibokenmodule/shibokenmodule.cpp +++ b/sources/shiboken6/shibokenmodule/shibokenmodule.cpp @@ -11,8 +11,8 @@ auto *pyType = reinterpret_cast(%2); if (Shiboken::ObjectType::checkType(pyType)) { auto *ptr = reinterpret_cast(%1); if (auto *wrapper = Shiboken::BindingManager::instance().retrieveWrapper(ptr)) { - Py_INCREF(wrapper); %PYARG_0 = reinterpret_cast(wrapper); + Py_INCREF(%PYARG_0); } else { %PYARG_0 = Shiboken::Object::newObject(pyType, ptr, false, true); } @@ -117,7 +117,7 @@ PyTuple_SetItem(version, 1, PyLong_FromLong(SHIBOKEN_MINOR_VERSION)); PyTuple_SetItem(version, 2, PyLong_FromLong(SHIBOKEN_MICRO_VERSION)); PyTuple_SetItem(version, 3, Shiboken::String::fromCString(SHIBOKEN_RELEASE_LEVEL)); PyTuple_SetItem(version, 4, PyLong_FromLong(SHIBOKEN_SERIAL)); -PyModule_AddObject(module, "__version_info__", version); +PepModule_Add(module, "__version_info__", version); PyModule_AddStringConstant(module, "__version__", SHIBOKEN_VERSION); VoidPtr::addVoidPtrToModule(module); Shiboken::initShibokenSupport(module); diff --git a/sources/shiboken6/tests/CMakeLists.txt b/sources/shiboken6/tests/CMakeLists.txt index 05f6e9e6..6de8199e 100644 --- a/sources/shiboken6/tests/CMakeLists.txt +++ b/sources/shiboken6/tests/CMakeLists.txt @@ -24,6 +24,11 @@ else() set(GENERATOR_EXTRA_FLAGS ) endif() list(APPEND GENERATOR_EXTRA_FLAGS ${SHIBOKEN_GENERATOR_EXTRA_FLAGS} ${debug_level}) +if (CMAKE_CROSSCOMPILING) + list(APPEND GENERATOR_EXTRA_FLAGS + "--platform=${CMAKE_SYSTEM_NAME}" "--arch=${CMAKE_SYSTEM_PROCESSOR}" + "--compiler-path=${CMAKE_CXX_COMPILER}") +endif() add_subdirectory(minimalbinding) if(NOT DEFINED MINIMAL_TESTS) diff --git a/sources/shiboken6/tests/libminimal/CMakeLists.txt b/sources/shiboken6/tests/libminimal/CMakeLists.txt index e1d7dda3..3bd31839 100644 --- a/sources/shiboken6/tests/libminimal/CMakeLists.txt +++ b/sources/shiboken6/tests/libminimal/CMakeLists.txt @@ -12,6 +12,7 @@ obj.cpp obj.h spanuser.cpp spanuser.h typedef.cpp typedef.h val.h +invisiblenamespace.h ) add_library(libminimal SHARED ${libminimal_SRC}) diff --git a/sources/shiboken6/tests/libminimal/invisiblenamespace.h b/sources/shiboken6/tests/libminimal/invisiblenamespace.h new file mode 100644 index 00000000..9ac8a705 --- /dev/null +++ b/sources/shiboken6/tests/libminimal/invisiblenamespace.h @@ -0,0 +1,15 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef INVISIBLENAMESPACE_H +#define INVISIBLENAMESPACE_H + +#include "libminimalmacros.h" + +namespace InvisibleNamespace::VisibleNamespace { + struct ValueType { + int i = 0; + }; +} + +#endif diff --git a/sources/shiboken6/tests/libsample/bucket.cpp b/sources/shiboken6/tests/libsample/bucket.cpp index cafd382a..43a32f41 100644 --- a/sources/shiboken6/tests/libsample/bucket.cpp +++ b/sources/shiboken6/tests/libsample/bucket.cpp @@ -40,15 +40,21 @@ bool Bucket::empty() void Bucket::lock() { - m_locked = true; - while (m_locked) { - SLEEP(300); + bool expected = false; + if (m_locked.compare_exchange_strong(expected, true)) { + while (m_locked) { + SLEEP(300); + } + } else { + std::cerr << __FUNCTION__ << " Attempt to lock twice.\n"; } } void Bucket::unlock() { - m_locked = false; + bool expected = true; + if (!m_locked.compare_exchange_strong(expected, false)) + std::cerr << __FUNCTION__ << " Attempt to unlock twice.\n"; } bool Bucket::virtualBlockerMethod() diff --git a/sources/shiboken6/tests/libsample/bucket.h b/sources/shiboken6/tests/libsample/bucket.h index 73e8edd7..677069fe 100644 --- a/sources/shiboken6/tests/libsample/bucket.h +++ b/sources/shiboken6/tests/libsample/bucket.h @@ -7,6 +7,7 @@ #include "libsamplemacros.h" #include "objecttype.h" +#include #include class ObjectType; @@ -19,7 +20,7 @@ public: int pop(); bool empty(); void lock(); - inline bool locked() { return m_locked; } + bool locked() { return m_locked.load(); } void unlock(); virtual bool virtualBlockerMethod(); @@ -27,8 +28,7 @@ public: private: std::list m_data; - - volatile bool m_locked = false; + std::atomic m_locked{false}; }; #endif // BUCKET_H diff --git a/sources/shiboken6/tests/libsample/bytearray.cpp b/sources/shiboken6/tests/libsample/bytearray.cpp index 78d5162b..a70950c6 100644 --- a/sources/shiboken6/tests/libsample/bytearray.cpp +++ b/sources/shiboken6/tests/libsample/bytearray.cpp @@ -59,7 +59,7 @@ ByteArray &ByteArray::append(char c) ByteArray &ByteArray::append(const char *data) { m_data.pop_back(); - std::copy(data, data + strlen(data), std::back_inserter(m_data)); + std::copy(data, data + std::strlen(data), std::back_inserter(m_data)); m_data.push_back('\0'); return *this; } diff --git a/sources/shiboken6/tests/libsample/functions.h b/sources/shiboken6/tests/libsample/functions.h index b745aed6..ef88e543 100644 --- a/sources/shiboken6/tests/libsample/functions.h +++ b/sources/shiboken6/tests/libsample/functions.h @@ -21,6 +21,8 @@ enum GlobalEnum { ThirdThing }; +using GlobalEnumAlias = GlobalEnum; + enum GlobalOverloadFuncEnum { GlobalOverloadFunc_i, GlobalOverloadFunc_d diff --git a/sources/shiboken6/tests/libsample/listuser.cpp b/sources/shiboken6/tests/libsample/listuser.cpp index 9bb7f779..c3b3295c 100644 --- a/sources/shiboken6/tests/libsample/listuser.cpp +++ b/sources/shiboken6/tests/libsample/listuser.cpp @@ -61,3 +61,14 @@ void ListUser::multiplyPointList(PointList &points, double multiplier) point->setY(point->y() * multiplier); } } + +std::vector ListUser::passThroughIntVector(const std::vector &v) +{ + return v; +} + +std::vector ListUser::passThroughBoolVector(const std::vector &v) +{ + return v; +} + diff --git a/sources/shiboken6/tests/libsample/listuser.h b/sources/shiboken6/tests/libsample/listuser.h index 69559cde..9b7a57c7 100644 --- a/sources/shiboken6/tests/libsample/listuser.h +++ b/sources/shiboken6/tests/libsample/listuser.h @@ -11,6 +11,7 @@ #include "libsamplemacros.h" #include +#include class LIBSAMPLE_API ListUser { @@ -45,6 +46,9 @@ public: inline void setList(std::list lst) { m_lst = lst; } inline std::list getList() const { return m_lst; } + static std::vector passThroughIntVector(const std::vector &v); + static std::vector passThroughBoolVector(const std::vector &v); + private: std::list m_lst; }; diff --git a/sources/shiboken6/tests/libsample/point.h b/sources/shiboken6/tests/libsample/point.h index 7e5d128a..61cc68ac 100644 --- a/sources/shiboken6/tests/libsample/point.h +++ b/sources/shiboken6/tests/libsample/point.h @@ -71,6 +71,4 @@ LIBSAMPLE_API bool operator!(const Point &pt); LIBSAMPLE_API Complex transmutePointIntoComplex(const Point &point); LIBSAMPLE_API Point transmuteComplexIntoPoint(const Complex &cpx); -LIBSAMPLE_API Point operator*(const Point &pt, double multiplier); - #endif // POINT_H diff --git a/sources/shiboken6/tests/libsample/pointf.h b/sources/shiboken6/tests/libsample/pointf.h index 49e00946..5e7fd7ee 100644 --- a/sources/shiboken6/tests/libsample/pointf.h +++ b/sources/shiboken6/tests/libsample/pointf.h @@ -60,6 +60,4 @@ LIBSAMPLE_API PointF operator*(int mult, const PointF &pt); LIBSAMPLE_API PointF operator-(const PointF &pt); LIBSAMPLE_API bool operator!(const PointF &pt); -LIBSAMPLE_API PointF operator*(const PointF &pt, double multiplier); - #endif // POINTF_H diff --git a/sources/shiboken6/tests/libsample/samplenamespace.cpp b/sources/shiboken6/tests/libsample/samplenamespace.cpp index 18a18d28..5e7b67a6 100644 --- a/sources/shiboken6/tests/libsample/samplenamespace.cpp +++ b/sources/shiboken6/tests/libsample/samplenamespace.cpp @@ -11,6 +11,16 @@ namespace SampleNamespace { +SomeClass::OptionAlias SomeClass::passThroughOptionAlias(OptionAlias ov) +{ + return ov; +} + +Option SomeClass::passThroughOption(Option ov) +{ + return ov; +} + // PYSIDE-817, scoped enums must not be converted to int in the wrappers generated // for the protected hacks SomeClass::PublicScopedEnum SomeClass::protectedMethodReturningPublicScopedEnum() const @@ -97,6 +107,15 @@ int passReferenceToObjectType(const ObjectType &obj, int multiplier) return obj.objectName().size() * multiplier; } +// Exercise specifying complete template specializations as primitive types. +std::optional optionalMultiply(const std::optional &v1, + const std::optional &v2) +{ + if (!v1.has_value() || !v2.has_value()) + return std::nullopt; + return v1.value() * v2.value(); +} + int variableInNamespace = 42; } // namespace SampleNamespace diff --git a/sources/shiboken6/tests/libsample/samplenamespace.h b/sources/shiboken6/tests/libsample/samplenamespace.h index 99a0787e..1613ce63 100644 --- a/sources/shiboken6/tests/libsample/samplenamespace.h +++ b/sources/shiboken6/tests/libsample/samplenamespace.h @@ -5,10 +5,12 @@ #define SAMPLENAMESPACE_H #include "libsamplemacros.h" +#include "samplenamespace.h" #include "str.h" #include "point.h" #include "objecttype.h" +#include #include // Anonymous global enum @@ -85,6 +87,12 @@ class LIBSAMPLE_API SomeClass public: enum class PublicScopedEnum { v1, v2 }; + // Alias an enumeration + using OptionAlias = Option; + inline static constexpr auto None_ = Option::None_; + inline static constexpr auto RandomNumber = Option::RandomNumber; + inline static constexpr auto UnixTime = Option::UnixTime; + class SomeInnerClass { public: @@ -106,16 +114,21 @@ public: inline int someMethod(SomeInnerClass *) { return 0; } virtual OkThisIsRecursiveEnough *someVirtualMethod(OkThisIsRecursiveEnough *arg) { return arg; } - }; + }; // OkThisIsRecursiveEnough protected: enum ProtectedEnum { ProtectedItem0, ProtectedItem1 }; - }; + }; // SomeInnerClass + struct SomeOtherInnerClass { std::list someInnerClasses; }; + + static OptionAlias passThroughOptionAlias(OptionAlias ov); + static Option passThroughOption(Option ov); + protected: enum ProtectedEnum { ProtectedItem0, @@ -157,6 +170,9 @@ LIBSAMPLE_API double passReferenceToValueType(const Point &point, double multipl // Add a new signature on type system with only a ObjectType pointer as parameter. LIBSAMPLE_API int passReferenceToObjectType(const ObjectType &obj, int multiplier); +LIBSAMPLE_API std::optional optionalMultiply(const std::optional &v1, + const std::optional &v2); + extern LIBSAMPLE_API int variableInNamespace; } // namespace SampleNamespace diff --git a/sources/shiboken6/tests/minimalbinding/CMakeLists.txt b/sources/shiboken6/tests/minimalbinding/CMakeLists.txt index 6eaae818..672b3f2b 100644 --- a/sources/shiboken6/tests/minimalbinding/CMakeLists.txt +++ b/sources/shiboken6/tests/minimalbinding/CMakeLists.txt @@ -15,6 +15,8 @@ ${CMAKE_CURRENT_BINARY_DIR}/minimal/val_wrapper.cpp ${CMAKE_CURRENT_BINARY_DIR}/minimal/listuser_wrapper.cpp ${CMAKE_CURRENT_BINARY_DIR}/minimal/spanuser_wrapper.cpp ${CMAKE_CURRENT_BINARY_DIR}/minimal/minbooluser_wrapper.cpp +${CMAKE_CURRENT_BINARY_DIR}/minimal/invisiblenamespace_visiblenamespace_wrapper.cpp +${CMAKE_CURRENT_BINARY_DIR}/minimal/invisiblenamespace_visiblenamespace_valuetype_wrapper.cpp ) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/minimal-binding.txt.in" diff --git a/sources/shiboken6/tests/minimalbinding/global.h b/sources/shiboken6/tests/minimalbinding/global.h index fc5c59a2..4f104b21 100644 --- a/sources/shiboken6/tests/minimalbinding/global.h +++ b/sources/shiboken6/tests/minimalbinding/global.h @@ -8,3 +8,4 @@ #include "listuser.h" #include "spanuser.h" #include "typedef.h" +#include "invisiblenamespace.h" diff --git a/sources/shiboken6/tests/minimalbinding/typesystem_minimal.xml b/sources/shiboken6/tests/minimalbinding/typesystem_minimal.xml index e18bf868..032ab92d 100644 --- a/sources/shiboken6/tests/minimalbinding/typesystem_minimal.xml +++ b/sources/shiboken6/tests/minimalbinding/typesystem_minimal.xml @@ -55,6 +55,12 @@ + + + + + + diff --git a/sources/shiboken6/tests/samplebinding/class_fields_test.py b/sources/shiboken6/tests/samplebinding/class_fields_test.py index 63b8b8fa..11052c5b 100644 --- a/sources/shiboken6/tests/samplebinding/class_fields_test.py +++ b/sources/shiboken6/tests/samplebinding/class_fields_test.py @@ -17,6 +17,9 @@ init_paths() from sample import Derived, Point, ObjectType +REF_COUNT_DELTA = 2 if sys.version_info >= (3, 14) else 1 + + class TestAccessingCppFields(unittest.TestCase): '''Simple test case for accessing the exposed C++ class fields.''' @@ -125,7 +128,7 @@ class TestAccessingCppFields(unittest.TestCase): refcount1 = sys.getrefcount(o1) d.objectTypeField = o1 self.assertEqual(d.objectTypeField, o1) - self.assertEqual(sys.getrefcount(d.objectTypeField), refcount1 + 1) + self.assertEqual(sys.getrefcount(d.objectTypeField), refcount1 + REF_COUNT_DELTA) # attributing a new object to instance's field should decrease the previous # object's reference count @@ -134,7 +137,7 @@ class TestAccessingCppFields(unittest.TestCase): d.objectTypeField = o2 self.assertEqual(d.objectTypeField, o2) self.assertEqual(sys.getrefcount(o1), refcount1) - self.assertEqual(sys.getrefcount(d.objectTypeField), refcount2 + 1) + self.assertEqual(sys.getrefcount(d.objectTypeField), refcount2 + REF_COUNT_DELTA) @unittest.skipUnless(hasattr(sys, "getrefcount"), f"{sys.implementation.name} has no refcount") def testRefCountingOfReferredObjectAfterDeletingReferrer(self): diff --git a/sources/shiboken6/tests/samplebinding/keep_reference_test.py b/sources/shiboken6/tests/samplebinding/keep_reference_test.py index 1c431763..0403efa9 100644 --- a/sources/shiboken6/tests/samplebinding/keep_reference_test.py +++ b/sources/shiboken6/tests/samplebinding/keep_reference_test.py @@ -15,6 +15,9 @@ init_paths() from sample import ObjectModel, ObjectView +REF_COUNT_DELTA = 2 if sys.version_info >= (3, 14) else 1 + + class TestKeepReference(unittest.TestCase): '''Test case for objects that keep references to other object without owning them (e.g. model/view relationships).''' @@ -26,15 +29,15 @@ class TestKeepReference(unittest.TestCase): refcount1 = sys.getrefcount(model1) view1 = ObjectView() view1.setModel(model1) - self.assertEqual(sys.getrefcount(view1.model()), refcount1 + 1) + self.assertEqual(sys.getrefcount(view1.model()), refcount1 + REF_COUNT_DELTA) view2 = ObjectView() view2.setModel(model1) - self.assertEqual(sys.getrefcount(view2.model()), refcount1 + 2) + self.assertEqual(sys.getrefcount(view2.model()), refcount1 + REF_COUNT_DELTA + 1) model2 = ObjectModel() view2.setModel(model2) - self.assertEqual(sys.getrefcount(view1.model()), refcount1 + 1) + self.assertEqual(sys.getrefcount(view1.model()), refcount1 + REF_COUNT_DELTA) @unittest.skipUnless(hasattr(sys, "getrefcount"), f"{sys.implementation.name} has no refcount") def testReferenceCountingWhenDeletingReferrer(self): @@ -43,7 +46,7 @@ class TestKeepReference(unittest.TestCase): refcount1 = sys.getrefcount(model) view = ObjectView() view.setModel(model) - self.assertEqual(sys.getrefcount(view.model()), refcount1 + 1) + self.assertEqual(sys.getrefcount(view.model()), refcount1 + REF_COUNT_DELTA) del view self.assertEqual(sys.getrefcount(model), refcount1) diff --git a/sources/shiboken6/tests/samplebinding/list_test.py b/sources/shiboken6/tests/samplebinding/list_test.py index 4d113722..37691c79 100644 --- a/sources/shiboken6/tests/samplebinding/list_test.py +++ b/sources/shiboken6/tests/samplebinding/list_test.py @@ -100,6 +100,16 @@ class ListConversionTest(unittest.TestCase): self.assertEqual(ListUser.ListOfPointF, ListUser.listOfPoints([PointF()])) self.assertEqual(ListUser.ListOfPoint, ListUser.listOfPoints([Point()])) + def testStdVector(self): + """PYSIDE-3259: Test std::vector in case some compiler + does the std::vector optimization.""" + intList = [1, 2] + actual = ListUser.passThroughIntVector(intList) + self.assertEqual(intList, actual) + boolList = [True, False] + actual = ListUser.passThroughBoolVector(boolList) + self.assertEqual(boolList, actual) + if __name__ == '__main__': unittest.main() diff --git a/sources/shiboken6/tests/samplebinding/namespace_test.py b/sources/shiboken6/tests/samplebinding/namespace_test.py index 0d67c749..0e92d6e2 100644 --- a/sources/shiboken6/tests/samplebinding/namespace_test.py +++ b/sources/shiboken6/tests/samplebinding/namespace_test.py @@ -64,6 +64,20 @@ class TestClassesUnderNamespace(unittest.TestCase): cls.setValue(SampleNamespace.EnumWithinInlineNamespace.EWIN_Value1) self.assertEqual(cls.value(), SampleNamespace.EnumWithinInlineNamespace.EWIN_Value1) + def testEnumAlias(self): + """Test whether an enumeration can be aliased to another one and values + can be used interchangeably.""" + expected = SampleNamespace.SomeClass.OptionAlias.None_ + actual = SampleNamespace.SomeClass.passThroughOptionAlias(expected) + self.assertEqual(expected, actual) + actual = SampleNamespace.SomeClass.passThroughOption(expected) + self.assertEqual(expected, actual) + # The alias source values should also work + actual = SampleNamespace.SomeClass.passThroughOptionAlias(SampleNamespace.Option.None_) + self.assertEqual(expected, actual) + actual = SampleNamespace.SomeClass.passThroughOption(SampleNamespace.Option.None_) + self.assertEqual(expected, actual) + if __name__ == '__main__': unittest.main() diff --git a/sources/shiboken6/tests/samplebinding/ownership_delete_parent_test.py b/sources/shiboken6/tests/samplebinding/ownership_delete_parent_test.py index 758ba835..d8ead530 100644 --- a/sources/shiboken6/tests/samplebinding/ownership_delete_parent_test.py +++ b/sources/shiboken6/tests/samplebinding/ownership_delete_parent_test.py @@ -48,9 +48,10 @@ class DeleteParentTest(unittest.TestCase): del parent # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion gc.collect() + EXPECTED_REF_COUNT = 3 if sys.version_info >= (3, 14) else 4 for i, child in enumerate(children): self.assertRaises(RuntimeError, child.objectName) - self.assertEqual(sys.getrefcount(child), 4) + self.assertEqual(sys.getrefcount(child), EXPECTED_REF_COUNT) @unittest.skipUnless(hasattr(sys, "getrefcount"), f"{sys.implementation.name} has no refcount") def testRecursiveParentDelete(self): @@ -62,10 +63,11 @@ class DeleteParentTest(unittest.TestCase): del parent # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion gc.collect() + EXPECTED_REF_COUNT = 1 if sys.version_info >= (3, 14) else 2 self.assertRaises(RuntimeError, child.objectName) - self.assertEqual(sys.getrefcount(child), 2) + self.assertEqual(sys.getrefcount(child), EXPECTED_REF_COUNT) self.assertRaises(RuntimeError, grandchild.objectName) - self.assertEqual(sys.getrefcount(grandchild), 2) + self.assertEqual(sys.getrefcount(grandchild), EXPECTED_REF_COUNT) if __name__ == '__main__': diff --git a/sources/shiboken6/tests/samplebinding/sample_test.py b/sources/shiboken6/tests/samplebinding/sample_test.py index c003ad39..43b84d36 100644 --- a/sources/shiboken6/tests/samplebinding/sample_test.py +++ b/sources/shiboken6/tests/samplebinding/sample_test.py @@ -79,6 +79,13 @@ class ModuleTest(unittest.TestCase): mo2 = sample.MoveOnlyHandler.passMoveOnly(mo) self.assertEqual(mo2.value(), v) + def testOptionalLong(self): + v1 = 2 + v2 = 3 + self.assertEqual(sample.SampleNamespace.optionalMultiply(v1, v2), 6) + self.assertIsNone(sample.SampleNamespace.optionalMultiply(v1, None)) + self.assertIsNone(sample.SampleNamespace.optionalMultiply(None, v2)) + if __name__ == '__main__': unittest.main() diff --git a/sources/shiboken6/tests/samplebinding/typesystem_sample.xml b/sources/shiboken6/tests/samplebinding/typesystem_sample.xml index 3f1b2e96..73f2e142 100644 --- a/sources/shiboken6/tests/samplebinding/typesystem_sample.xml +++ b/sources/shiboken6/tests/samplebinding/typesystem_sample.xml @@ -120,6 +120,25 @@ + + + + if (!%in.has_value()) + Py_RETURN_NONE; + return PyLong_FromLong(%in.value()); + + + + SBK_UNUSED(%in) + %out = %OUTTYPE(); + + + %out = %OUTTYPE(PyLong_AsLong(%in)); + + + + + @@ -401,6 +420,7 @@ --> + @@ -418,6 +438,10 @@ + + + + @@ -1231,7 +1255,7 @@ - PyObject* new_arg0 = PyLong_FromLong(PyLong_AS_LONG(%PYARG_1) - %2); + PyObject* new_arg0 = PyLong_FromLong(PyLong_AsLong(%PYARG_1) - %2); Py_DECREF(%PYARG_1); %PYARG_1 = new_arg0; @@ -1920,7 +1944,7 @@ - ByteArray b(Py_TYPE(%PYSELF)->tp_name); + ByteArray b(PepType_GetFullyQualifiedNameStr(Py_TYPE(%PYSELF))); PyObject* aux = Shiboken::String::fromStringAndSize(%CPPSELF.data(), %CPPSELF.size()); if (PyUnicode_CheckExact(aux)) { PyObject* tmp = PyUnicode_AsASCIIString(aux); @@ -2048,7 +2072,7 @@ // Does nothing really, just test the code generation // of constructors whose arguments where - long %out = PyLong_AS_LONG(%PYARG_1) + 1; + long %out = PyLong_AsLong(%PYARG_1) + 1; diff --git a/sources/shiboken6/tests/smartbinding/std_optional_test.py b/sources/shiboken6/tests/smartbinding/std_optional_test.py index 10e72f12..9e8bc8d4 100644 --- a/sources/shiboken6/tests/smartbinding/std_optional_test.py +++ b/sources/shiboken6/tests/smartbinding/std_optional_test.py @@ -26,6 +26,15 @@ def integer_from_value(v): class StdOptionalTests(unittest.TestCase): + def testConversionFromInt(self): + """PYSIDE-3107: Test whether a parameter taking a 'std::optional' + accepts 'int'.""" + b = StdOptionalTestBench() + b.setOptionalInt(43) + self.assertEqual(b.optionalInt().value(), 43) + b.setOptionalInt(None) + self.assertFalse(b.optionalInt().has_value()) + def testCInt(self): b = StdOptionalTestBench() ci = b.optionalInt() @@ -42,8 +51,6 @@ class StdOptionalTests(unittest.TestCase): ci = std.optional_int(43) self.assertEqual(ci.value(), 43) - @unittest.skipIf(True, """PYSIDE-2854, T &std::optional::value() does not work/ - returns self (colocated).""") def testInteger(self): b = StdOptionalTestBench() i = b.optionalInteger() diff --git a/sources/shiboken6/tests/smartbinding/typesystem_smart.xml b/sources/shiboken6/tests/smartbinding/typesystem_smart.xml index 14b181b6..4024036f 100644 --- a/sources/shiboken6/tests/smartbinding/typesystem_smart.xml +++ b/sources/shiboken6/tests/smartbinding/typesystem_smart.xml @@ -50,6 +50,19 @@ value-check-method="has_value" instantiations="Integer,int"> + + + + + SBK_UNUSED(pyIn) + %out = std::nullopt; + + + %OUTTYPE_0 v = %CONVERTTOCPP[%OUTTYPE_0](%in); + %out = %OUTTYPE(v); + + + diff --git a/testing/runner.py b/testing/runner.py index 4d96fdfc..c85c4f15 100644 --- a/testing/runner.py +++ b/testing/runner.py @@ -214,11 +214,13 @@ class TestRunner: os.rename(tmp_name, self.logfile) self.partial = True else: + _ = ctest_process.communicate() self.partial = False finally: print("End of the test run") print() tee_process.wait() + ctest_process.wait() def run(self, label, rerun, timeout): cmd = self.ctestCommand, "--output-log", self.logfile diff --git a/testing/wheel_tester.py b/testing/wheel_tester.py index e5b47ffc..5ac9333d 100644 --- a/testing/wheel_tester.py +++ b/testing/wheel_tester.py @@ -143,7 +143,7 @@ def generate_build_cmake(): # Specify prefix path so find_package(Qt6) works. qmake_dir = Path(QMAKE_PATH).resolve().parent.parent args = [CMAKE_PATH, "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", - f"-Dpython_interpreter={sys.executable}", + f"-DPython_EXECUTABLE={sys.executable}", f"-DCMAKE_PREFIX_PATH={qmake_dir}", ".."] exit_code = run_process(args) diff --git a/tools/create_changelog.py b/tools/create_changelog.py index a0d49d83..14adddba 100644 --- a/tools/create_changelog.py +++ b/tools/create_changelog.py @@ -50,7 +50,7 @@ def change_log(version: list) -> Path: def is_lts_version(version: list) -> bool: - return version[0] == 5 or version[1] in (2, 5) + return version[0] == 5 or version[1] in (2, 5, 8) def version_tag(version: list) -> str: diff --git a/tools/cross_compile_android/main.py b/tools/cross_compile_android/main.py index 6636d080..e37ab511 100644 --- a/tools/cross_compile_android/main.py +++ b/tools/cross_compile_android/main.py @@ -104,6 +104,9 @@ if __name__ == "__main__": parser.add_argument("-v", "--verbose", help="run in verbose mode", action="store_const", dest="loglevel", const=logging.INFO) + # As opposed to Qt, Qt for Python does not require API level 28 because it can be built with a + # higher API for toolchain compatibility, while still remaining compatible with Qt's runtime + # minimum. parser.add_argument("--api-level", type=str, default="35", help="Minimum Android API level to use") parser.add_argument("--ndk-path", type=str, help="Path to Android NDK (Preferred r26b)") diff --git a/tools/doc_modules.py b/tools/doc_modules.py index a26811a9..243dd28f 100644 --- a/tools/doc_modules.py +++ b/tools/doc_modules.py @@ -118,6 +118,8 @@ def _write_global_header(modules, file): """Helper to write the global header for shiboken.""" for module in modules: print(f"#include <{module}/{module}>", file=file) + if module == "QtGui": + print("#include ", file=file) def write_global_header(modules, filename): diff --git a/tools/example_gallery/main.py b/tools/example_gallery/main.py index 8dc0789f..6469c0c3 100644 --- a/tools/example_gallery/main.py +++ b/tools/example_gallery/main.py @@ -2,19 +2,7 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only from __future__ import annotations - -""" -This tool reads all the examples from the main repository that have a -'.pyproject' file, and generates a special table/gallery in the documentation -page. - -For the usage, simply run: - python tools/example_gallery/main.py -since there is no special requirements. -""" - -import json -import math +import fnmatch import os import shutil import zipfile @@ -23,8 +11,12 @@ from argparse import ArgumentParser, RawTextHelpFormatter from dataclasses import dataclass from enum import IntEnum, Enum from pathlib import Path -from textwrap import dedent from collections import defaultdict +from typing import DefaultDict + +sys.path.append(os.fspath(Path(__file__).parent.parent.parent / "sources" / "pyside-tools")) +from project_lib import parse_pyproject_json, parse_pyproject_toml, \ + PYPROJECT_FILE_PATTERNS, PYPROJECT_TOML_PATTERN, PYPROJECT_JSON_PATTERN # noqa: E402 class Format(Enum): @@ -32,41 +24,39 @@ class Format(Enum): MD = 1 -class ModuleType(IntEnum): - ESSENTIALS = 0 - ADDONS = 1 - M2M = 2 - - -SUFFIXES = {Format.RST: "rst", Format.MD: "md"} - - -opt_quiet = False - +__doc__ = """\ +This tool scans the main repository for examples with project files and generates a documentation +page formatted as a gallery, displaying the examples in a table +For the usage, simply run: + python tools/example_gallery/main.py +""" +DIR = Path(__file__).parent +EXAMPLES_DOC = Path(f"{DIR}/../../sources/pyside6/doc/examples").resolve() +EXAMPLES_DIR = Path(f"{DIR}/../../examples/").resolve() +TARGET_HELP = f"Directory into which to generate Doc files (default: {str(EXAMPLES_DOC)})" +BASE_URL = "https://code.qt.io/cgit/pyside/pyside-setup.git/tree" +DOC_SUFFIXES = {Format.RST: "rst", Format.MD: "md"} LITERAL_INCLUDE = ".. literalinclude::" - - IMAGE_SUFFIXES = (".png", ".jpg", ".jpeg", ".gif", ".svg", ".svgz", ".webp") - - +# Suffixes to ignore when displaying source files that are referenced in the project file IGNORED_SUFFIXES = IMAGE_SUFFIXES + (".pdf", ".pyc", ".obj", ".mesh") - - -suffixes = { - ".h": "cpp", - ".cpp": "cpp", - ".md": "markdown", - ".py": "py", - ".qml": "js", - ".conf": "ini", - ".qrc": "xml", - ".ui": "xml", - ".xbel": "xml", - ".xml": "xml", +LANGUAGE_PATTERNS = { + "*.h": "cpp", + "*.cpp": "cpp", + "*.md": "markdown", + "*.py": "py", + "*.qml": "js", + "*.qmlproject": "js", + "*.conf": "ini", + "*.qrc": "xml", + "*.ui": "xml", + "*.xbel": "xml", + "*.xml": "xml", + "*.html": "html", + "CMakeLists.txt": "cmake", } - BASE_CONTENT = """\ .. _pyside6_examples: @@ -82,42 +72,59 @@ Examples directory. """ +# We generate a 'toctree' at the end of the file to include the new 'example' rst files, so we get +# no warnings and also that users looking for them will be able to, since they are indexed +# Notice that :hidden: will not add the list of files by the end of the main examples HTML page. +FOOTER_INDEX = """\ +.. toctree:: + :hidden: + :maxdepth: 1 +""" +TUTORIAL_HEADLINES = { + "tutorials/extending-qml/chapter": "Tutorial: Writing QML Extensions with Python", + "tutorials/extending-qml-advanced/advanced": "Tutorial: Writing advanced QML Extensions with" + "Python", + "tutorials/finance_manager": "Tutorial: Finance Manager - Integrating PySide6 with SQLAlchemy " + "and FastAPI", +} -def tutorial_headline(path: str): - if "tutorials/extending-qml/chapter" in path: - return "Tutorial: Writing QML Extensions with Python" - if "tutorials/extending-qml-advanced/advanced" in path: - return "Tutorial: Writing advanced QML Extensions with Python" - if "tutorials/finance_manager" in path: - return "Tutorial: Finance Manager - Integrating PySide6 with SQLAlchemy and FastAPI" - return "" + +class ModuleType(IntEnum): + ESSENTIALS = 0 + ADDONS = 1 + M2M = 2 -def ind(x): - return " " * 4 * x +def get_lexer(path: Path) -> str: + """Given a file path, return the language lexer to use for syntax highlighting""" + for pattern, lexer in LANGUAGE_PATTERNS.items(): + if fnmatch.fnmatch(path.name, pattern): + return lexer + # Default to text + return "text" -def get_lexer(path): - if path.name == "CMakeLists.txt": - return "cmake" - lexer = suffixes.get(path.suffix) - return lexer if lexer else "text" +def ind(level: int) -> str: + """Return a string of spaces for string indentation given certain level""" + return " " * 4 * level -def add_indent(s, level): +def add_indent(s: str, level: int) -> str: + """Add indentation to a string""" new_s = "" for line in s.splitlines(): if line.strip(): new_s += f"{ind(level)}{line}\n" else: + # Empty line new_s += "\n" return new_s -def check_img_ext(i): - """Check whether path is an image.""" - return i.suffix in IMAGE_SUFFIXES +def check_img_ext(image_path: Path) -> bool: + """Check whether a file path is an image depending on its suffix.""" + return image_path.suffix in IMAGE_SUFFIXES @dataclass @@ -177,15 +184,15 @@ MODULE_DESCRIPTIONS = { } -def module_sort_key(name): - """Return key for sorting modules.""" +def module_sort_key(name: str) -> str: + """Return a key for sorting the Qt modules.""" description = MODULE_DESCRIPTIONS.get(name) module_type = int(description.module_type) if description else 5 sort_key = description.sort_key if description else 100 return f"{module_type}:{sort_key:04}:{name}" -def module_title(name): +def module_title(name: str) -> str: """Return title for a module.""" result = name.title() description = MODULE_DESCRIPTIONS.get(name) @@ -205,25 +212,22 @@ def module_title(name): class ExampleData: """Example data for formatting the gallery.""" - def __init__(self): - self.headline = "" - - example: str - module: str - extra: str - doc_file: str - file_format: Format - abs_path: str - has_doc: bool - img_doc: Path - headline: str - tutorial: str + example_name: str = None + module: str = None + extra: str = None + doc_file: str = None + file_format: Format = None + abs_path: str = None + src_doc_file: Path = None + img_doc: Path = None + tutorial: str = None + headline: str = "" -def get_module_gallery(examples): +def get_module_gallery(examples: list[ExampleData]) -> str: """ - This function takes a list of dictionaries, that contain examples - information, from one specific module. + This function takes a list of examples from one specific module and returns the resulting string + in RST format that can be used to generate the table for the examples """ gallery = ( @@ -231,25 +235,22 @@ def get_module_gallery(examples): f"{ind(2)}:gutter: 3\n\n" ) - # Iteration per rows - for i in range(math.ceil(len(examples))): - e = examples[i] - suffix = SUFFIXES[e.file_format] + for i, example in enumerate(examples): + suffix = DOC_SUFFIXES[example.file_format] # doc_file with suffix removed, to be used as a sphinx reference - doc_file_name = e.doc_file.replace(f".{suffix}", "") # lower case sphinx reference # this seems to be a bug or a requirement from sphinx - doc_file_name = doc_file_name.lower() + doc_file_name = example.doc_file.replace(f".{suffix}", "").lower() - name = e.example - underline = e.module + name = example.example_name + underline = example.module - if e.extra: - underline += f"/{e.extra}" + if example.extra: + underline += f"/{example.extra}" if i > 0: gallery += "\n" - img_name = e.img_doc.name if e.img_doc else "../example_no_image.png" + img_name = example.img_doc.name if example.img_doc else "../example_no_image.png" # Fix long names if name.startswith("chapter"): @@ -259,28 +260,17 @@ def get_module_gallery(examples): # Handling description from original file desc = "" - original_dir = Path(e.abs_path) / "doc" - - if e.has_doc: - # cannot use e.doc_file because that is the target file name - # so finding the original file by the name - original_file = (next(original_dir.glob("*.rst"), None) - or next(original_dir.glob("*.md"), None)) - if not original_file: - # ideally won't reach here because has_doc is True - print(f"example_gallery: No .rst or .md file found in {original_dir}") - continue - - with original_file.open("r", encoding="utf-8") as f: + if example.src_doc_file: + with example.src_doc_file.open("r", encoding="utf-8") as f: # Read the first line first_line = f.readline().strip() # Check if the first line is a reference (starts with '(' and ends with ')=' for MD, # or starts with '.. ' and ends with '::' for RST) - if ((e.file_format == Format.MD and first_line.startswith('(') + if ((example.file_format == Format.MD and first_line.startswith('(') and first_line.endswith(')=')) - or (e.file_format == Format.RST and first_line.startswith('.. ') - and first_line.endswith('::'))): + or (example.file_format == Format.RST and first_line.startswith('.. ') + and first_line.endswith('::'))): # The first line is a reference, so read the next lines until a non-empty line # is found while True: @@ -294,21 +284,17 @@ def get_module_gallery(examples): # The next line handling depends on the file format line = f.readline().strip() - if e.file_format == Format.MD: + if example.file_format == Format.MD: # For markdown, the second line is the empty line - if line != "": - # If the line is not empty, raise a runtime error - raise RuntimeError(f"Unexpected line: {line} in {original_file}. " - "Needs handling.") + pass else: - # For RST and other formats - # The second line is the underline under the title - _ = line + # For RST and other formats, the second line is the underline under the title # The next line should be empty line = f.readline().strip() - if line != "": - raise RuntimeError(f"Unexpected line: {line} in {original_file}. " - "Needs handling.") + + if line != "": + raise RuntimeError( + f"{line} was expected to be empty. Doc file: {example.src_doc_file}") # Now read until another empty line lines = [] @@ -328,10 +314,14 @@ def get_module_gallery(examples): if len(desc) > 120: desc = desc[:120] + "..." else: - print(f"example_gallery: No .rst or .md file found in {original_dir}") - - title = e.headline - if not title: + if not opt_quiet: + print( + f"example_gallery: No source doc file found in {example.abs_path}." + f"Skipping example", + file=sys.stderr, + ) + + if not (title := example.headline): title = f"{name} from ``{underline}``." # Clean refs from desc @@ -344,28 +334,30 @@ def get_module_gallery(examples): gallery += f"{ind(3)}:link: {doc_file_name}\n" gallery += f"{ind(3)}:link-type: ref\n" gallery += f"{ind(3)}:img-top: {img_name}\n\n" - gallery += f"{ind(3)}+++\n{ind(3)}{desc}\n" + gallery += f"{ind(3)}+++\n" + gallery += f"{ind(3)}{desc}\n" return f"{gallery}\n" -def remove_licenses(s): +def remove_licenses(file_content: str) -> str: + # Return the content of the file with the Qt license removed new_s = [] - for line in s.splitlines(): + for line in file_content.splitlines(): if line.strip().startswith(("/*", "**", "##")): continue new_s.append(line) return "\n".join(new_s) -def make_zip_archive(zip_file, src, skip_dirs=None): - src_path = Path(src).expanduser().resolve(strict=True) +def make_zip_archive(output_path: Path, src: Path, skip_dirs: list[str] = None): + # Create a .zip file from a source directory ignoring the specified directories + src_path = src.expanduser().resolve(strict=True) if skip_dirs is None: skip_dirs = [] if not isinstance(skip_dirs, list): - print("Error: A list needs to be passed for 'skip_dirs'") - return - with zipfile.ZipFile(zip_file, 'w', zipfile.ZIP_DEFLATED) as zf: + raise ValueError("Type error: 'skip_dirs' must be a list instance") + with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf: for file in src_path.rglob('*'): skip = False _parts = file.relative_to(src_path).parts @@ -377,75 +369,76 @@ def make_zip_archive(zip_file, src, skip_dirs=None): zf.write(file, file.relative_to(src_path.parent)) -def doc_file(project_dir, project_file_entry): - """Return the (optional) .rstinc file describing a source file.""" +def get_rstinc_for_file(project_dir: Path, project_file: Path) -> Path | None: + """Return the .rstinc file in the doc folder describing a source file, if found""" rst_file = project_dir - if rst_file.name != "doc": # Special case: Dummy .pyproject file in doc dir + if project_dir.name != "doc": # Special case: Dummy .pyproject file in doc dir rst_file /= "doc" - rst_file /= Path(project_file_entry).name + ".rstinc" + rst_file /= project_file.name + ".rstinc" return rst_file if rst_file.is_file() else None -def get_code_tabs(files, project_dir, file_format): +def get_code_tabs(files: list[Path], project_dir: Path, file_format: Format) -> str: + """ + Return the string which contains the code tabs source for the example + Also creates a .zip file for downloading the source files + """ content = "\n" # Prepare ZIP file, and copy to final destination - # Handle examples which only have a dummy pyproject file in the "doc" dir - zip_root = project_dir.parent if project_dir.name == "doc" else project_dir - zip_name = f"{zip_root.name}.zip" - make_zip_archive(EXAMPLES_DOC / zip_name, zip_root, skip_dirs=["doc"]) + zip_name = f"{project_dir.name}.zip" + make_zip_archive(EXAMPLES_DOC / zip_name, project_dir, skip_dirs=["doc"]) if file_format == Format.RST: content += f":download:`Download this example <{zip_name}>`\n\n" - else: + elif file_format == Format.MD: content += f"{{download}}`Download this example <{zip_name}>`\n\n" + # MD files wrap the content in a eval-rst block content += "```{eval-rst}\n" + else: + raise ValueError(f"Unknown documentation file format {file_format}") - for i, project_file in enumerate(files): - if i == 0: - content += ".. tab-set::\n\n" + if files: + content += ".. tab-set::\n\n" - pfile = Path(project_file) - if pfile.suffix in IGNORED_SUFFIXES: + for i, file in enumerate(files): + if file.suffix in IGNORED_SUFFIXES: continue - content += f"{ind(1)}.. tab-item:: {project_file}\n\n" + try: + tab_title = file.relative_to(project_dir).as_posix() + except ValueError: + # The file is outside project_dir, so the best we can do is to use the file name + tab_title = file.name + + content += f"{ind(1)}.. tab-item:: {tab_title}\n\n" - doc_rstinc_file = doc_file(project_dir, project_file) - if doc_rstinc_file: - indent = ind(2) - for line in doc_rstinc_file.read_text("utf-8").split("\n"): - content += indent + line + "\n" + if doc_rstinc_file := get_rstinc_for_file(project_dir, file): + content += add_indent(doc_rstinc_file.read_text("utf-8"), 2) content += "\n" - lexer = get_lexer(pfile) - content += add_indent(f"{ind(1)}.. code-block:: {lexer}", 1) + content += add_indent(f"{ind(1)}.. code-block:: {get_lexer(file)}", 1) content += "\n" - _path = project_dir / project_file - _file_content = "" try: - with open(_path, "r", encoding="utf-8") as _f: - _file_content = remove_licenses(_f.read()) + file_content = remove_licenses(file.read_text(encoding="utf-8")) except UnicodeDecodeError as e: - print(f"example_gallery: error decoding {project_dir}/{_path}:{e}", - file=sys.stderr) - raise + raise RuntimeError(f"example_gallery: error decoding {file}: {e}") except FileNotFoundError as e: - print(f"example_gallery: error opening {project_dir}/{_path}:{e}", - file=sys.stderr) - raise + raise RuntimeError(f"example_gallery: error opening {file}: {e}") - content += add_indent(_file_content, 3) + content += add_indent(file_content, 3) content += "\n\n" if file_format == Format.MD: + # Close the eval-rst block content += "```" return content -def get_header_title(example_dir): +def get_default_header_title(example_dir: Path) -> str: + """Get a default header title for an example directory without a doc file""" _index = example_dir.parts.index("examples") rel_path = "/".join(example_dir.parts[_index:]) _title = rel_path @@ -459,24 +452,29 @@ def get_header_title(example_dir): ) -def rel_path(from_path, to_path): - """Determine relative paths for paths that are not subpaths (where - relative_to() fails) via a common root.""" - common = Path(*os.path.commonprefix([from_path.parts, to_path.parts])) - up_dirs = len(from_path.parts) - len(common.parts) +def rel_path(from_path: Path, to_path: Path) -> str: + """ + Get a relative path for a given path that is not a subpath (where Path.relative_to() fails) + of from_path via a common ancestor path + + For example: from_path = Path("/a/b/c/d"), to_path = Path("/a/b/e/f"). Returns: "../../e/f" + """ + common_path = Path(*os.path.commonprefix([from_path.parts, to_path.parts])) + up_dirs = len(from_path.parts) - len(common_path.parts) prefix = up_dirs * "../" - rel_to_common = os.fspath(to_path.relative_to(common)) - return f"{prefix}{rel_to_common}" + relative_to_common = to_path.relative_to(common_path).as_posix() + return f"{prefix}{relative_to_common}" -def read_rst_file(project_dir, project_files, doc_rst): - """Read the example .rst file and expand literal includes to project files - by relative paths to the example directory. Note: sphinx does not - handle absolute paths as expected, they need to be relative.""" - content = "" - with open(doc_rst, encoding="utf-8") as doc_f: - content = doc_f.read() +def read_rst_file(project_dir: Path, project_files: list[Path], doc_rst: Path) -> str: + """ + Read the example .rst file and replace Sphinx literal includes of project files by paths + relative to the example directory + Note: Sphinx does not handle absolute paths as expected, they need to be relative + """ + content = Path(doc_rst).read_text(encoding="utf-8") if LITERAL_INCLUDE not in content: + # The file does not contain any literal includes, so we can return it as is return content result = [] @@ -484,14 +482,16 @@ def read_rst_file(project_dir, project_files, doc_rst): for line in content.split("\n"): if line.startswith(LITERAL_INCLUDE): file = line[len(LITERAL_INCLUDE) + 1:].strip() - if file in project_files: - line = f"{LITERAL_INCLUDE} {path_to_example}/{file}" + file_path = project_dir / file + if file_path not in project_files: + raise RuntimeError(f"File {file} not found in project files: {project_files}") + line = f"{LITERAL_INCLUDE} {path_to_example}/{file}" result.append(line) return "\n".join(result) -def get_headline(text, file_format): - """Find the headline in the .rst file.""" +def get_headline(text: str, file_format: Format) -> str: + """Find the headline in the documentation file.""" if file_format == Format.RST: underline = text.find("\n====") if underline != -1: @@ -503,23 +503,32 @@ def get_headline(text, file_format): new_line = text.find("\n", headline + 1) if new_line != -1: return text[headline + 2:new_line].strip() + else: + raise ValueError(f"Unknown file format {file_format}") return "" -def get_doc_source_file(original_doc_dir, example_name): - """Find the doc source file, return (Path, Format).""" - if original_doc_dir.is_dir(): - for file_format in (Format.RST, Format.MD): - suffix = SUFFIXES[file_format] - result = original_doc_dir / f"{example_name}.{suffix}" - if result.is_file(): - return result, file_format - return None, Format.RST +def get_doc_source_file(original_doc_dir: Path, example_name: str) -> tuple[Path, Format] | None: + """ + Find the doc source file given a doc directory and an example name + Returns the doc file path and the file format, if found + """ + if not original_doc_dir.is_dir(): + return None + + for file_format, suffix in DOC_SUFFIXES.items(): + result = original_doc_dir / f"{example_name}.{suffix}" + if result.is_file(): + return result, file_format + return None -def get_screenshot(image_dir, example_name): - """Find screen shot: We look for an image with the same - example_name first, if not, we select the first.""" +def get_screenshot(image_dir: Path, example_name: str) -> Path | None: + """ + Find an screenshot given an image directory and the example name + Returns the image with the same example_name, if found + If not found, the first image in the directory is returned + """ if not image_dir.is_dir(): return None images = [i for i in image_dir.glob("*") if i.is_file() and check_img_ext(i)] @@ -531,36 +540,30 @@ def get_screenshot(image_dir, example_name): return None -def write_resources(src_list, dst): +def write_resources(src_list: list[Path], dst: Path): """Write a list of example resource paths to the dst path.""" for src in src_list: resource_written = shutil.copy(src, dst / src.name) if not opt_quiet: - print("Written resource:", resource_written) + print(f"Written resource: {resource_written}") @dataclass class ExampleParameters: """Parameters obtained from scanning the examples directory.""" - - def __init__(self): - self.file_format = Format.RST - self.src_doc_dir = self.src_doc_file_path = self.src_screenshot = None - self.extra_names = "" - - example_dir: Path - module_name: str - example_name: str - extra_names: str - file_format: Format - target_doc_file: str - src_doc_dir: Path - src_doc_file_path: Path - src_screenshot: Path - - -def detect_pyside_example(example_root, pyproject_file): - """Detemine parameters of a PySide example.""" + example_dir: Path = None + module_name: str = "" + example_name: str = "" + target_doc_file: str = None + extra_names: str = "" + src_doc_dir: Path = None + src_doc_file_path: Path = None + src_screenshot: Path = None + file_format: Format = Format.RST + + +def get_pyside_example_parameters(example_root: Path, pyproject_file: Path) -> ExampleParameters: + """Analyze a PySide example folder to get the example parameters""" p = ExampleParameters() p.example_dir = pyproject_file.parent @@ -568,7 +571,7 @@ def detect_pyside_example(example_root, pyproject_file): # Design Studio project example p.example_dir = pyproject_file.parent.parent - if p.example_dir.name == "doc": # Dummy pyproject in doc dir (scriptableapplication) + if p.example_dir.name == "doc": # Dummy pyproject file in doc dir (e.g. scriptableapplication) p.example_dir = p.example_dir.parent parts = p.example_dir.parts[len(example_root.parts):] @@ -581,21 +584,23 @@ def detect_pyside_example(example_root, pyproject_file): src_doc_dir = p.example_dir / "doc" if src_doc_dir.is_dir(): - src_doc_file_path, fmt = get_doc_source_file(src_doc_dir, p.example_name) - if src_doc_file_path: - p.src_doc_file_path = src_doc_file_path - p.file_format = fmt + src_doc_file = get_doc_source_file(src_doc_dir, p.example_name) + if src_doc_file: + p.src_doc_file_path, p.file_format = src_doc_file p.src_doc_dir = src_doc_dir p.src_screenshot = get_screenshot(src_doc_dir, p.example_name) - target_suffix = SUFFIXES[p.file_format] + target_suffix = DOC_SUFFIXES[p.file_format] doc_file = f"example_{p.module_name}_{p.extra_names}_{p.example_name}.{target_suffix}" p.target_doc_file = doc_file.replace("__", "_") return p -def detect_qt_example(example_root, pyproject_file): - """Detemine parameters of an example from a Qt repository.""" +def get_qt_example_parameters(pyproject_file: Path) -> ExampleParameters: + """ + Analyze a Qt repository example to get its parameters. + For instance, the qtdoc/examples/demos/mediaplayer example + """ p = ExampleParameters() p.example_dir = pyproject_file.parent @@ -604,142 +609,155 @@ def detect_qt_example(example_root, pyproject_file): # Check for a 'doc' directory inside the example (qdoc) doc_root = p.example_dir / "doc" if doc_root.is_dir(): - src_doc_file_path, fmt = get_doc_source_file(doc_root / "src", p.example_name) - if src_doc_file_path: - p.src_doc_file_path = src_doc_file_path - p.file_format = fmt + src_doc_file = get_doc_source_file(doc_root / "src", p.example_name) + if src_doc_file: + p.src_doc_file_path, p.file_format = src_doc_file p.src_doc_dir = doc_root p.src_screenshot = get_screenshot(doc_root / "images", p.example_name) - - target_suffix = SUFFIXES[p.file_format] + else: + raise ValueError(f"No source file found in {doc_root} / src given {p.example_name}") + else: + raise ValueError(f"No doc directory found in {p.example_dir}") + target_suffix = DOC_SUFFIXES[p.file_format] p.target_doc_file = f"example_qtdemos_{p.example_name}.{target_suffix}" return p -def write_example(example_root, pyproject_file, pyside_example=True): - """Read the project file and documentation, create the .rst file and - copy the data. Return a tuple of module name and a dict of example data.""" - p = (detect_pyside_example(example_root, pyproject_file) if pyside_example - else detect_qt_example(example_root, pyproject_file)) +def write_example( + example_root: Path, pyproject_file: Path, pyside_example: bool = True +) -> tuple[str, ExampleData]: + """ + Read the project file and documentation, create the .rst file and copy the example data + Return a tuple with the module name and an ExampleData instance + """ + # Get the example parameters depending on whether it is a PySide example or a Qt one + p: ExampleParameters = ( + get_pyside_example_parameters(example_root, pyproject_file) + if pyside_example else get_qt_example_parameters(pyproject_file)) result = ExampleData() - result.example = p.example_name + result.example_name = p.example_name result.module = p.module_name result.extra = p.extra_names result.doc_file = p.target_doc_file result.file_format = p.file_format result.abs_path = str(p.example_dir) - result.has_doc = bool(p.src_doc_file_path) + result.src_doc_file = p.src_doc_file_path result.img_doc = p.src_screenshot - result.tutorial = tutorial_headline(result.abs_path) - - files = [] - try: - with pyproject_file.open("r", encoding="utf-8") as pyf: - pyproject = json.load(pyf) - # iterate through the list of files in .pyproject and - # check if they exist, before appending to the list. - for f in pyproject["files"]: - if not Path(f).exists: - print(f"example_gallery: {f} listed in {pyproject_file} does not exist") - raise FileNotFoundError - else: - files.append(f) - except (json.JSONDecodeError, KeyError, FileNotFoundError) as e: - print(f"example_gallery: error reading {pyproject_file}: {e}") - raise + result.tutorial = TUTORIAL_HEADLINES.get(result.abs_path, "") + if pyproject_file.match(PYPROJECT_JSON_PATTERN): + pyproject_parse_result = parse_pyproject_json(pyproject_file) + elif pyproject_file.match(PYPROJECT_TOML_PATTERN): + pyproject_parse_result = parse_pyproject_toml(pyproject_file) + else: + raise RuntimeError(f"Invalid project file: {pyproject_file}") + + if pyproject_parse_result.errors: + raise RuntimeError(f"Error reading {pyproject_file}: {pyproject_parse_result.errors}") + + for file in pyproject_parse_result.files: + if not Path(file).exists: + raise FileNotFoundError(f"{file} listed in {pyproject_file} does not exist") + + files = pyproject_parse_result.files headline = "" if files: doc_file = EXAMPLES_DOC / p.target_doc_file - sphnx_ref_example = p.target_doc_file.replace(f'.{SUFFIXES[p.file_format]}', '') + sphnx_ref_example = p.target_doc_file.replace(f'.{DOC_SUFFIXES[p.file_format]}', '') # lower case sphinx reference # this seems to be a bug or a requirement from sphinx sphnx_ref_example = sphnx_ref_example.lower() - content_f = "" + if p.file_format == Format.RST: content_f = f".. _{sphnx_ref_example}:\n\n" elif p.file_format == Format.MD: content_f = f"({sphnx_ref_example})=\n\n" else: - print(f"example_gallery: Invalid file format {p.file_format}", file=sys.stderr) - raise ValueError + raise ValueError(f"Invalid file format {p.file_format}") with open(doc_file, "w", encoding="utf-8") as out_f: if p.src_doc_file_path: content_f += read_rst_file(p.example_dir, files, p.src_doc_file_path) headline = get_headline(content_f, p.file_format) - if not headline: - print(f"example_gallery: No headline found in {doc_file}", - file=sys.stderr) + if not headline and not opt_quiet: + print(f"example_gallery: No headline found in {doc_file}", file=sys.stderr) - # Copy other files in the 'doc' directory, but - # excluding the main '.rst' file and all the - # directories. + # Copy other files in the 'doc' directory, but excluding the main '.rst' file and + # all the directories resources = [] if pyside_example: for _f in p.src_doc_dir.glob("*"): if _f != p.src_doc_file_path and not _f.is_dir(): resources.append(_f) - else: # Qt example: only use image. - if p.src_screenshot: - resources.append(p.src_screenshot) + elif p.src_screenshot: + # Qt example: only use image, if found + resources.append(p.src_screenshot) write_resources(resources, EXAMPLES_DOC) else: - content_f += get_header_title(p.example_dir) - content_f += get_code_tabs(files, pyproject_file.parent, p.file_format) + content_f += get_default_header_title(p.example_dir) + content_f += get_code_tabs(files, p.example_dir, p.file_format) out_f.write(content_f) if not opt_quiet: print(f"Written: {doc_file}") else: if not opt_quiet: - print("Empty '.pyproject' file, skipping") + print(f"{pyproject_file} does not contain any file, skipping") result.headline = headline - return (p.module_name, result) + return p.module_name, result -def example_sort_key(example: ExampleData): +def example_sort_key(example: ExampleData) -> str: + """ + Return a key for sorting the examples. Tutorials are sorted first, then the examples which + contain "gallery" in their name, then alphabetically + """ result = "" if example.tutorial: result += "AA:" + example.tutorial + ":" - elif "gallery" in example.example: + elif "gallery" in example.example_name: result += "AB:" - result += example.example + result += example.example_name return result -def sort_examples(example): +def sort_examples(examples: dict[str, list[ExampleData]]) -> dict[str, list[ExampleData]]: + """Sort the examples using a custom function.""" result = {} - for module in example.keys(): - result[module] = sorted(example.get(module), key=example_sort_key) + for module in examples.keys(): + result[module] = sorted(examples.get(module), key=example_sort_key) return result -def scan_examples_dir(examples_dir, pyside_example=True): - """Scan a directory of examples.""" - for pyproject_file in examples_dir.glob("**/*.pyproject"): - if pyproject_file.name != "examples.pyproject": - module_name, data = write_example(examples_dir, pyproject_file, - pyside_example) - if module_name not in examples: - examples[module_name] = [] - examples[module_name].append(data) +def scan_examples_dir( + examples_dir: Path, pyside_example: bool = True +) -> dict[str, list[ExampleData]]: + """ + Scan a directory of examples and return a dictionary with the found examples grouped by module + Also creates the .rst file for each example + """ + examples: dict[str, list[ExampleData]] = defaultdict(list) + # Find all the project files contained in the examples directory + project_files: list[Path] = [] + for project_file_pattern in PYPROJECT_FILE_PATTERNS: + project_files.extend(examples_dir.glob(f"**/{project_file_pattern}")) + + for project_file in project_files: + if project_file.name == "examples.pyproject": + # Ignore this project file. It contains files from many examples + continue + + module_name, data = write_example(examples_dir, project_file, pyside_example) + examples[module_name].append(data) + return dict(examples) -if __name__ == "__main__": - # Only examples with a '.pyproject' file will be listed. - DIR = Path(__file__).parent - EXAMPLES_DOC = Path(f"{DIR}/../../sources/pyside6/doc/examples").resolve() - EXAMPLES_DIR = Path(f"{DIR}/../../examples/").resolve() - BASE_URL = "https://code.qt.io/cgit/pyside/pyside-setup.git/tree" - columns = 5 - gallery = "" +if __name__ == "__main__": parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter) - TARGET_HELP = f"Directory into which to generate Doc files (default: {str(EXAMPLES_DOC)})" parser.add_argument("--target", "-t", action="store", dest="target_dir", help=TARGET_HELP) parser.add_argument("--qt-src-dir", "-s", action="store", help="Qt source directory") parser.add_argument("--quiet", "-q", action="store_true", help="Quiet") @@ -748,55 +766,43 @@ if __name__ == "__main__": if options.target_dir: EXAMPLES_DOC = Path(options.target_dir).resolve() - # This main loop will be in charge of: - # * Getting all the .pyproject files, - # * Gather the information of the examples and store them in 'examples' - # * Read the .pyproject file to output the content of each file - # on the final .rst file for that specific example. - examples = {} + # This script will be in charge of: + # * Getting all the project files + # * Gather the information of the examples + # * Read the project file to output the content of each source file + # on the final .rst file for that specific example # Create the 'examples' directory if it doesn't exist - # If it does exist, remove it and create a new one to start fresh + # If it does exist, try to remove it and create a new one to start fresh if EXAMPLES_DOC.is_dir(): shutil.rmtree(EXAMPLES_DOC, ignore_errors=True) if not opt_quiet: - print("WARNING: Deleted old html directory") + print("WARNING: Deleted existing examples HTML directory") EXAMPLES_DOC.mkdir(exist_ok=True) - scan_examples_dir(EXAMPLES_DIR) + examples = scan_examples_dir(EXAMPLES_DIR) + if options.qt_src_dir: + # Scan the Qt source directory for Qt examples and include them in the dictionary of + # discovered examples qt_src = Path(options.qt_src_dir) if not qt_src.is_dir(): - print("Invalid Qt source directory: {}", file=sys.stderr) - sys.exit(-1) - scan_examples_dir(qt_src.parent / "qtdoc", pyside_example=False) + raise RuntimeError(f"Invalid Qt source directory: {qt_src}") + examples.update(scan_examples_dir(qt_src.parent / "qtdoc", pyside_example=False)) examples = sort_examples(examples) - # We generate a 'toctree' at the end of the file, to include the new - # 'example' rst files, so we get no warnings, and also that users looking - # for them will be able to, since they are indexed. - # Notice that :hidden: will not add the list of files by the end of the - # main examples HTML page. - footer_index = dedent( - """\ - .. toctree:: - :hidden: - :maxdepth: 1 - - """ - ) - - # Writing the main example rst file. - index_files = [] + # List of all the example files found to be included in the index table of contents + index_files: list[str] = [] + # Write the main example .rst file and the example files with open(f"{EXAMPLES_DOC}/index.rst", "w") as f: f.write(BASE_CONTENT) for module_name in sorted(examples.keys(), key=module_sort_key): - e = examples.get(module_name) - tutorial_examples = defaultdict(list) - non_tutorial_examples = [] + module_examples = examples.get(module_name) + tutorial_examples: DefaultDict[str, list[ExampleData]] = defaultdict(list) + non_tutorial_examples: list[ExampleData] = [] - for example in e: + for example in module_examples: index_files.append(example.doc_file) if example.tutorial: tutorial_examples[example.tutorial].append(example) @@ -817,12 +823,14 @@ if __name__ == "__main__": f.write(get_module_gallery(non_tutorial_examples)) # If no tutorials exist, list all examples elif not tutorial_examples: - f.write(get_module_gallery(e)) + f.write(get_module_gallery(module_examples)) f.write("\n\n") - f.write(footer_index) - for i in index_files: - f.write(f" {i}\n") + + # Add the list of the example files found to the index table of contents + f.write(FOOTER_INDEX) + for index_file in index_files: + f.write(f"{ind(1)}{index_file}\n") if not opt_quiet: print(f"Written index: {EXAMPLES_DOC}/index.rst") diff --git a/tools/release_notes/main.py b/tools/release_notes/main.py index 77ce4742..1fd0fd9a 100644 --- a/tools/release_notes/main.py +++ b/tools/release_notes/main.py @@ -36,17 +36,27 @@ This section contains the release notes for different versions of Qt for Python. """ +JIRA_URL = "https://qt-project.atlassian.net/browse/" + + class Changelog: + + # for matching lines like * PySide6 * to identify the section + section_pattern = re.compile(r"\* +(\w+) +\*") + # for line that start with ' -' which lists the changes + line_pattern = re.compile(r"^ -") + # for line that contains a bug report like PYSIDE- + bug_number_pattern = re.compile(r"\[PYSIDE-\d+\]") + # version from changelog file name + version_pattern = re.compile(r"changes-(\d+)\.(\d+)\.(\d+)") + def __init__(self, file_path: Path): self.file_path = file_path - self.version = file_path.name.split("-")[-1] + version_match = self.version_pattern.match(file_path.name) + assert (version_match) + self.version = (int(version_match.group(1)), int(version_match.group(2)), + int(version_match.group(3))) self.sections = {section: [] for section in SECTION_NAMES} - # for matching lines like * PySide6 * to identify the section - self.section_pattern = re.compile(r"\* +(\w+) +\*") - # for line that start with ' -' which lists the changes - self.line_pattern = re.compile(r"^ -") - # for line that contains a bug report like PYSIDE- - self.bug_number_pattern = re.compile(r"\[PYSIDE-\d+\]") def add_line(self, section, line): self.sections[section].append(line) @@ -94,8 +104,7 @@ class Changelog: # remove the square brackets actual_bug_number = bug_number[1:-1] bug_number_replacement = ( - f"[{actual_bug_number}]" - f"(https://bugreports.qt.io/browse/{actual_bug_number})" + f"[{actual_bug_number}]({JIRA_URL}/{actual_bug_number})" ) line = re.sub(re.escape(bug_number), bug_number_replacement, line) @@ -144,7 +153,8 @@ def write_md_file(section: str, changelogs: list[Changelog], output_dir: Path): for changelog in changelogs: section_contents = changelog.parsed_sections()[section] if section_contents: - file.write(f"## {changelog.version}\n\n") + v = changelog.version + file.write(f"## {v[0]}.{v[1]}.{v[2]}\n\n") for lines in section_contents: # separate each line with a newline file.write(f"{lines}\n") diff --git a/tools/sync_examples.py b/tools/sync_examples.py new file mode 100644 index 00000000..c2715f41 --- /dev/null +++ b/tools/sync_examples.py @@ -0,0 +1,199 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import os +import shutil +import sys +from pathlib import Path +from argparse import ArgumentParser, RawDescriptionHelpFormatter + +USAGE = """ +Updates example images, shaders, *.qml, *.ui, *.qrc and qmldir files from +a Qt source tree. + +Check the diffs produced with care ("prefer" in qmldir, QML module +definitions). +""" + +BINARY_SUFFIXES = ["jpg", "png", "svgz", "webp"] +TEXT_SUFFIXES = ["frag", "qrc", "qml", "svg", "ui", "vert"] +SUFFIXES = BINARY_SUFFIXES + TEXT_SUFFIXES + + +QML_SIMPLE_TUTORIAL_NAMES = ["chapter1-basics", "chapter2-methods", + "chapter3-bindings", "chapter4-customPropertyTypes", + "chapter5-listproperties", "chapter6-plugins"] +QML_SIMPLE_TUTORIALS = ["qml/tutorials/extending-qml/" + n for n in QML_SIMPLE_TUTORIAL_NAMES] + +QML_ADVANCED_TUTORIAL_NAMES = ["advanced1-Base-project", "advanced2-Inheritance-and-coercion", + "advanced3-Default-properties", "advanced4-Grouped-properties", + "advanced5-Attached-properties", "advanced6-Property-value-source"] +QML_ADVANCED_TUTORIALS = ["qml/tutorials/extending-qml-advanced/" + n + for n in QML_ADVANCED_TUTORIAL_NAMES] + +EXAMPLE_MAPPING = { + "qtbase": ["corelib/ipc/sharedmemory", "gui/rhiwindow", "sql/books", + "widgets/animation/easing", "widgets/rhi/simplerhiwidget"], + "qtconnectivity": ["bluetooth/heartrate_game", "bluetooth/lowenergyscanner"], + "qtdeclarative": (QML_SIMPLE_TUTORIALS + QML_ADVANCED_TUTORIALS + + ["quick/models/stringlistmodel", "quick/models/objectlistmodel", + "quick/window", + "quick/rendercontrol/rendercontrol_opengl", + "quick/scenegraph/openglunderqml", + "quick/scenegraph/scenegraph_customgeometry", + "quick/customitems/painteditem", + "quickcontrols/filesystemexplorer", "quickcontrols/gallery"]), + "qtgraphs": ["graphs/2d/hellographs", "graphs/3d/bars", "graphs/3d/widgetgraphgallery"], + "qtlocation": ["location/mapviewer"], + "qtmultimedia": ["multimedia/camera"], + "qtquick3d": ["quick3d/customgeometry", "quick3d/intro", "quick3d/proceduraltexture"], + "qtserialbus": ["serialbus/can", "serialbus/modbus/modbusclient"], + "qtserialport": ["serialport/terminal"], + "qtspeech": ["speech/hello_speak"], + "qtwebchannel": ["webchannel/standalone"], + "qtwebengine": ["pdfwidgets/pdfviewer", "webenginequick/nanobrowser", + "webenginewidgets/notifications", "webenginewidgets/simplebrowser"], + "qtwebview": ["webview/minibrowser"], +} + + +file_count = 0 +updated_file_count = 0 +new_file_count = 0 +warnings_count = 0 + + +def pyside_2_qt_example(e): + """Fix some example names differing in PySide.""" + if "heartrate" in e: + return e.replace("heartrate_", "heartrate-") + if e == "webenginequick/nanobrowser": + return "webenginequick/quicknanobrowser" + if e.endswith("scenegraph_customgeometry"): + return e.replace("scenegraph_customgeometry", "customgeometry") + if e.endswith("modbusclient"): + return e.replace("modbusclient", "client") + return e + + +def files_differ(p1, p2): + return (p1.stat().st_size != p2.stat().st_size + or p1.read_bytes() != p2.read_bytes()) + + +def use_file(path): + """Exclude C++ docs and Qt Creator builds.""" + path_str = os.fspath(path) + return "/doc/" not in path_str and "_install_" not in path_str + + +def example_sources(qt_example): + """Retrieve all update-able files of a Qt C++ example.""" + result = [] + for suffix in SUFFIXES: + for file in qt_example.glob(f"**/*.{suffix}"): + if use_file(file): + result.append(file) + for file in qt_example.glob("**/qmldir*"): + if use_file(file): + result.append(file) + return result + + +def detect_qml_module(pyside_example, sources): + """Detect the directory of a QML module of a PySide example. + While in Qt C++, the QML module's .qml files are typically + located in the example root, PySide has an additional directory + since it loads the QML files from the file system. + Read the qmldir file and check whether a module directory exists.""" + qml_dir_file = None + for source in sources: + if source.name.startswith("qmldir"): # "qmldir"/"qmldir.in" + qml_dir_file = source + break + if not qml_dir_file: + return None + for line in qml_dir_file.read_text(encoding="utf-8").split("\n"): + if line.startswith("module "): + module = line[7:].strip() + if (pyside_example / module).is_dir(): + return module + break + return None + + +def sync_example(pyside_example, qt_example, dry_run): + """Update files of a PySide example.""" + global file_count, updated_file_count, new_file_count, warnings_count + sources = example_sources(qt_example) + source_count = len(sources) + if source_count == 0: + print(f"No sources found in {qt_example}", file=sys.stderr) + return + count = 0 + qml_module = detect_qml_module(pyside_example, sources) + for source in sources: + rel_source = source.relative_to(qt_example) + target = pyside_example / rel_source + if qml_module and not target.is_file(): + target = pyside_example / qml_module / rel_source + if target.is_file(): + if files_differ(source, target): + if not dry_run: + shutil.copy(source, target) + count += 1 + else: + print(f"{qt_example.name}: {rel_source} does not have an equivalent " + "PySide file, skipping", file=sys.stderr) + warnings_count += 1 + new_file_count += 1 + if count > 0: + print(f" {qt_example.name:<30}: Updated {count}/{source_count} files(s)") + else: + print(f" {qt_example.name:<30}: Unchanged, {source_count} files(s)") + file_count += source_count + updated_file_count += count + + +def main(): + global warnings_count + parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter, + description=USAGE) + parser.add_argument("--dry-run", action="store_true", help="show the files to be updated") + parser.add_argument('qtsource', nargs=1) + args = parser.parse_args() + dry_run = args.dry_run + qt_source = Path(args.qtsource[0]) + if not qt_source.is_dir(): + raise Exception(f"{qt_source} is not a directory") + + pyside_examples = Path(__file__).parents[1].resolve() / "examples" + print(qt_source, '->', pyside_examples) + + for qt_module, example_list in EXAMPLE_MAPPING.items(): + for example in example_list: + pyside_example = pyside_examples / example + qt_example = (qt_source / qt_module / "examples" + / pyside_2_qt_example(example)) + if pyside_example.is_dir() and qt_example.is_dir(): + sync_example(pyside_example, qt_example, dry_run) + else: + print(f"Invalid mapping {qt_example} -> {pyside_example}", + file=sys.stderr) + warnings_count += 1 + msg = f"Updated {updated_file_count}/{file_count} file(s)" + if new_file_count: + msg += f", {new_file_count} new files(s)" + if warnings_count: + msg += f", {warnings_count} warning(s)" + print(f"\n{msg}.\n") + return 0 + + +if __name__ == "__main__": + r = -1 + try: + r = main() + except Exception as e: + print(str(e), file=sys.stderr) + sys.exit(r) diff --git a/wheel_artifacts/pyproject.toml.base b/wheel_artifacts/pyproject.toml.base index ad9f12c4..b652f3e2 100644 --- a/wheel_artifacts/pyproject.toml.base +++ b/wheel_artifacts/pyproject.toml.base @@ -10,7 +10,7 @@ authors = [ description = "PROJECT_DESCRIPTION" readme = "PROJECT_README" dynamic = ["version"] -requires-python = ">=3.9, <3.14" +requires-python = ">=3.9, <3.15" keywords = ["Qt"] license = {text="LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only"} dependencies = "PROJECT_DEPENDENCIES" @@ -34,6 +34,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Database", "Topic :: Software Development", "Topic :: Software Development :: Code Generators",