From: Patrick Franz Date: Sat, 12 Apr 2025 17:34:19 +0000 (+0200) Subject: Import kf6-kirigami_6.13.0.orig.tar.xz X-Git-Tag: archive/raspbian/6.13.0-1+rpi1^2~2 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=4ccedf4c9ce528f85709749d16f269a79b0f17c0;p=kf6-kirigami.git Import kf6-kirigami_6.13.0.orig.tar.xz [dgit import orig kf6-kirigami_6.13.0.orig.tar.xz] --- 4ccedf4c9ce528f85709749d16f269a79b0f17c0 diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..7c4e327 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,3 @@ +#clang-tidy +ae47f7f9553bd222123acc77f336753840173c57 +5530f66e057634db4158cfe83b8ee0d14a47aad3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..110143a --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Ignore the following files +*~ +*.[oa] +*.diff +*.kate-swp +*.kdev4 +.kdev_include_paths +*.kdevelop.pcs +*.moc +*.moc.cpp +*.orig +*.user +.*.swp +.swp.* +Doxyfile +Makefile +avail +random_seed +/build*/ +/.vscode/ +CMakeLists.txt.user* +*.unc-backup* +.cmake/ +.vscode/ +/.clang-format +/compile_commands.json +.clangd +.idea +/cmake-build* +.cache +.qmlls.ini +.qmllint.ini diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..ded8a6f --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2020 Volker Krause +# SPDX-License-Identifier: CC0-1.0 + +include: + - project: sysadmin/ci-utilities + file: + - /gitlab-templates/linux-qt6.yml + - /gitlab-templates/linux-qt6-next.yml + - /gitlab-templates/linux-qt6-static.yml + - /gitlab-templates/android-qt6.yml + - /gitlab-templates/windows-qt6.yml + - /gitlab-templates/freebsd-qt6.yml + - /gitlab-templates/alpine-qt6.yml + - /gitlab-templates/xml-lint.yml + - /gitlab-templates/yaml-lint.yml diff --git a/.kde-ci.yml b/.kde-ci.yml new file mode 100644 index 0000000..ce14be1 --- /dev/null +++ b/.kde-ci.yml @@ -0,0 +1,10 @@ +Dependencies: + - 'on': ['Linux', 'FreeBSD', 'Windows', 'Android', 'iOS'] + 'require': + 'frameworks/extra-cmake-modules': '@same' + +Options: + test-before-installing: True + require-passing-tests-on: ['Linux', 'FreeBSD', 'Windows'] + cmake-options: -DBUILD_EXAMPLES=ON + cppcheck-ignore-files: ['templates/'] diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2017611 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,190 @@ +cmake_minimum_required(VERSION 3.16) + +set(KF_VERSION "6.13.0") # handled by release scripts +set(KF_DEP_VERSION "6.13.0") # handled by release scripts + +project(kirigami2 VERSION ${KF_VERSION}) + +set(REQUIRED_QT_VERSION 6.6.0) + +################# Disallow in-source build ################# + +if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") + message(FATAL_ERROR "kirigami requires an out of source build. Please create a separate build directory and run 'cmake path_to_kirigami [options]' there.") +endif() + +option(BUILD_SHARED_LIBS "Build a shared module" ON) +option(DESKTOP_ENABLED "Build and install The Desktop style" ON) +option(BUILD_EXAMPLES "Build and install examples" OFF) +option(UBUNTU_TOUCH "Build for Ubuntu Touch" OFF) +if(DEFINED STATIC_LIBRARY) + message(FATAL_ERROR "Use the BUILD_SHARED_LIBS=OFF option to build a static library, STATIC_LIBRARY is no longer a supported option") +endif() +find_package(ECM 6.13.0 REQUIRED NO_MODULE) +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH}) + +if (NOT ${BUILD_SHARED_LIBS}) + # Examples are not supported when building a static library, so force them + # to OFF. + set(BUILD_EXAMPLES OFF) +endif() + +include(FeatureSummary) +include(KDEInstallDirs) +find_package(Qt6 ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE COMPONENTS Core Quick Gui Svg QuickControls2 Concurrent ShaderTools) + +if (Qt6Gui_VERSION VERSION_GREATER_EQUAL "6.10.0") + find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) +endif() + +if (BUILD_TESTING) + find_package(Qt6QuickTest ${REQUIRED_QT_VERSION} CONFIG QUIET) +endif() +get_target_property(QtGui_Enabled_Features Qt6::Gui QT_ENABLED_PUBLIC_FEATURES) +if(QtGui_Enabled_Features MATCHES "opengl") + set(HAVE_QTGUI_OPENGL 1) +else() + set(HAVE_QTGUI_OPENGL 0) +endif() +add_feature_info(QtGuiOpenGL HAVE_QTGUI_OPENGL "QtGui built with support for OpenGL") +set(CMAKE_AUTOMOC ON) +set(AUTOMOC_MOC_OPTIONS -Muri=org.kde.kirigami) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +if(NOT BUILD_SHARED_LIBS) + add_definitions(-DKIRIGAMI_BUILD_TYPE_STATIC) + add_definitions(-DQT_PLUGIN) + add_definitions(-DQT_STATICPLUGIN=1) +endif() + +################# set KDE specific information ################# +# where to look first for cmake modules, before ${CMAKE_ROOT}/Modules/ is checked +set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules") + +include(ECMGenerateExportHeader) +include(ECMSetupVersion) +include(ECMGenerateHeaders) +include(CMakePackageConfigHelpers) +include(ECMPoQmTools) +include(ECMFindQmlModule) +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDEGitCommitHooks) +include(ECMQtDeclareLoggingCategory) +include(ECMAddQch) +include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) +include(KDEPackageAppTemplates) +include(ECMQmlModule) +include(ECMDeprecationSettings) + +set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF6Kirigami") + +option(BUILD_QCH "Build API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)" OFF) +add_feature_info(QCH ${BUILD_QCH} "API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)") + +ecm_setup_version(PROJECT + VARIABLE_PREFIX KIRIGAMI + VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/kirigami_version.h" + PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF6KirigamiConfigVersion.cmake" + SOVERSION 6 +) + +ecm_setup_version(PROJECT + VARIABLE_PREFIX KIRIGAMI + PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF6Kirigami2ConfigVersion.cmake" + SOVERSION 6 +) + +# shall we use DBus? +# enabled per default on Linux & BSD systems +set(USE_DBUS_DEFAULT OFF) +if(UNIX AND NOT APPLE AND NOT ANDROID AND NOT HAIKU) + set(USE_DBUS_DEFAULT ON) +endif() +option(USE_DBUS "Build components using DBus" ${USE_DBUS_DEFAULT}) +if(USE_DBUS) + find_package(Qt6DBus ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) + set(WITH_DBUS ON) + add_definitions(-DKIRIGAMI_ENABLE_DBUS) +endif() + +find_package(OpenMP) +set_package_properties(OpenMP + PROPERTIES DESCRIPTION "Multi-platform shared-memory parallel programming in C/C++ and Fortran" + TYPE OPTIONAL + PURPOSE "Accelerates palette generation in Kirigami.ImageColors" +) +if(OpenMP_CXX_FOUND) + set(HAVE_OpenMP TRUE) + set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") +endif() + +include_directories("${CMAKE_CURRENT_BINARY_DIR}") +configure_file(config-OpenMP.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-OpenMP.h) + +if (UBUNTU_TOUCH) + add_definitions(-DUBUNTU_TOUCH) +endif() + +ecm_set_disabled_deprecation_versions( + QT 6.2 + KF 6.12.0 +) + +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + # link time optimization has been observed to break colors. Force-disable it. + # Before undoing this make double sure the lockscreen + # and logout screen are correctly colored in light/dark mode! + add_compile_options("-fno-lto") +endif() + +add_subdirectory(src) +if (NOT ANDROID) + add_subdirectory(templates) +endif() + +if (BUILD_EXAMPLES) + add_subdirectory(examples) +endif() + +if (BUILD_TESTING) + add_subdirectory(autotests) +endif() + +configure_package_config_file( + "KF6KirigamiConfig.cmake.in" + "KF6KirigamiConfig.cmake" + INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} + PATH_VARS CMAKE_INSTALL_PREFIX +) + +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/KF6KirigamiConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/KF6KirigamiConfigVersion.cmake" + "${CMAKE_CURRENT_SOURCE_DIR}/KF6KirigamiMacros.cmake" + DESTINATION "${CMAKECONFIG_INSTALL_DIR}" + COMPONENT Devel +) + +configure_package_config_file( + "KF6Kirigami2Config.cmake.in" + "KF6Kirigami2Config.cmake" + INSTALL_DESTINATION "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF6Kirigami2" + PATH_VARS CMAKE_INSTALL_PREFIX +) + +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/KF6Kirigami2Config.cmake + "${CMAKE_CURRENT_BINARY_DIR}/KF6Kirigami2ConfigVersion.cmake" + DESTINATION ${KDE_INSTALL_CMAKEPACKAGEDIR}/KF6Kirigami2 + COMPONENT Devel +) + +ecm_install_po_files_as_qm(poqm) + +include(ECMFeatureSummary) +ecm_feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) + +configure_file(qmllint.ini.in ${CMAKE_SOURCE_DIR}/.qmllint.ini) + +kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) diff --git a/ExtraDesktop.sh b/ExtraDesktop.sh new file mode 100644 index 0000000..0c46ec1 --- /dev/null +++ b/ExtraDesktop.sh @@ -0,0 +1,4 @@ +#! /bin/sh +#This file outputs in a separate line each file with a .desktop syntax +#that needs to be translated but has a non .desktop extension +find -name \*.kdevtemplate -print diff --git a/KF6Kirigami2Config.cmake.in b/KF6Kirigami2Config.cmake.in new file mode 100644 index 0000000..13405b5 --- /dev/null +++ b/KF6Kirigami2Config.cmake.in @@ -0,0 +1,9 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +message(AUTHOR_WARNING "find_package(KF6Kirigami2) is deprecated, use find_package(KF6Kirigami) instead") +find_dependency(KF6Kirigami @PROJECT_VERSION@) + +if (NOT TARGET KF6::Kirigami2) + add_library(KF6::Kirigami2 ALIAS KF6Kirigami) +endif() diff --git a/KF6KirigamiConfig.cmake.in b/KF6KirigamiConfig.cmake.in new file mode 100644 index 0000000..eae80e4 --- /dev/null +++ b/KF6KirigamiConfig.cmake.in @@ -0,0 +1,25 @@ +@PACKAGE_INIT@ +# Any changes in this ".cmake" file will be overwritten by CMake, the source is the ".cmake.in" file. + +include(CMakeFindDependencyMacro) +find_dependency(Qt6Core @REQUIRED_QT_VERSION@) +find_dependency(Qt6Qml @REQUIRED_QT_VERSION@) +find_dependency(Qt6Quick @REQUIRED_QT_VERSION@) +find_dependency(Qt6Concurrent @REQUIRED_QT_VERSION@) + +if (@HAVE_OpenMP@) + find_dependency(OpenMP) +endif() + +find_dependency(KF6KirigamiPlatform @PROJECT_VERSION@) + +include("${CMAKE_CURRENT_LIST_DIR}/KF6KirigamiTargets.cmake") + +if (NOT TARGET KF6::Kirigami AND TARGET KF6Kirigami) + add_library(KF6::Kirigami ALIAS KF6Kirigami) +endif() + +set(Kirigami_INSTALL_PREFIX "@PACKAGE_CMAKE_INSTALL_PREFIX@") + +include("${CMAKE_CURRENT_LIST_DIR}/KF6KirigamiMacros.cmake") +@PACKAGE_INCLUDE_QCHTARGETS@ diff --git a/KF6KirigamiMacros.cmake b/KF6KirigamiMacros.cmake new file mode 100644 index 0000000..140181c --- /dev/null +++ b/KF6KirigamiMacros.cmake @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: 2016 Marco Martin +# SPDX-FileCopyrightText: 2023 Volker Krause +# SPDX-FileCopyrightText: 2025 Carl Schwan +# SPDX-License-Identifier: BSD-2-Clause + +include(CMakeParseArguments) +include(ExternalProject) + +function(kirigami_package_breeze_icons) + set(_multiValueArgs ICONS) + cmake_parse_arguments(ARG "" "" "${_multiValueArgs}" ${ARGN} ) + + if(NOT ARG_ICONS) + message(FATAL_ERROR "No ICONS argument given to kirigami_package_breeze_icons") + endif() + + if(NOT ANDROID) + return() # not needed on other platforms + endif() + + #include icons used by Kirigami components themselves + set(ARG_ICONS ${ARG_ICONS} + application-exit + dialog-close + dialog-error + dialog-information + dialog-positive + dialog-warning + edit-clear-locationbar-ltr + edit-clear-locationbar-rtl + edit-copy + edit-delete-remove + emblem-error + emblem-information + emblem-success + emblem-warning + globe + go-next + go-next-symbolic + go-next-symbolic-rtl + go-previous + go-previous-symbolic + go-previous-symbolic-rtl + go-up + handle-sort + mail-sent + open-menu-symbolic + overflow-menu-left + overflow-menu-right + overflow-menu + password-show-off + password-show-on + tools-report-bug + user + view-left-new + view-right-new + view-left-close + view-right-close + ) + + function(_find_breeze_icon icon) + foreach(_size 48 32 22 16 12) + SET(path "") + file(GLOB_RECURSE path ${_BREEZEICONS_DIR}/icons/*/${_size}/${icon}.svg) + if (path STREQUAL "") + continue() + endif() + + list(LENGTH path _count_paths) + if (_count_paths GREATER 1) + message(WARNING "Found more than one version of '${icon}': ${path}") + endif() + list(GET path 0 path) + + get_filename_component(realpath "${path}" REALPATH) + if (EXISTS ${realpath}) + install(FILES ${realpath} DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami/breeze-internal/icons/${_size}) + endif() + + # Create direct symlink if original icon was also a symlink + # We can't reuse the existing symlink because often it's a chain + # of symlink and we can only get the final destination. + if (NOT "${realpath}" MATCHES "${path}") + get_filename_component(filename "${realpath}" NAME) + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${_size}/") + file(CREATE_LINK ${filename} "${CMAKE_CURRENT_BINARY_DIR}/${_size}/${icon}.svg" SYMBOLIC) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${_size}/${icon}.svg" + DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami/breeze-internal/icons/${_size}) + endif() + + endforeach() + endfunction() + + if (BREEZEICONS_DIR AND NOT EXISTS ${BREEZEICONS_DIR}) + message(FATAL_ERROR "BREEZEICONS_DIR variable does not point to existing dir: \"${BREEZEICONS_DIR}\"") + endif() + + set(_BREEZEICONS_DIR "${BREEZEICONS_DIR}") + + #FIXME: this is a terrible hack + if(NOT _BREEZEICONS_DIR) + set(_BREEZEICONS_DIR "${CMAKE_BINARY_DIR}/breeze-icons/src/breeze-icons") + + # replacement for ExternalProject_Add not yet working + # first time config? + if (NOT EXISTS ${_BREEZEICONS_DIR}) + find_package(Git) + execute_process(COMMAND ${GIT_EXECUTABLE} clone --depth 1 https://invent.kde.org/frameworks/breeze-icons.git ${_BREEZEICONS_DIR}) + endif() + endif() + + message (STATUS "Found external breeze icons:") + foreach(_iconName ${ARG_ICONS}) + _find_breeze_icon(${_iconName}) + endforeach() + + #generate an index.theme that qiconloader can understand + file(WRITE ${CMAKE_BINARY_DIR}/index.theme "[Icon Theme]\nName=Breeze\nDirectories=icons/12,icons/16,icons/22,icons/32,icons/48,icons\nFollowsColorScheme=true\n[icons/12]\nSize=12\nType=Fixed\n[icons/16]\nSize=16\nType=Fixed\n[icons/22]\nSize=22\nType=Fixed\n[icons/32]\nSize=32\nType=Fixed\n[icons/48]\nSize=48\nType=Fixed\n[icons]\nSize=32\nType=Scalable\n") + install(FILES ${CMAKE_BINARY_DIR}/index.theme DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami/breeze-internal/) +endfunction() diff --git a/LICENSES/BSD-2-Clause.txt b/LICENSES/BSD-2-Clause.txt new file mode 100644 index 0000000..baa80b5 --- /dev/null +++ b/LICENSES/BSD-2-Clause.txt @@ -0,0 +1,22 @@ +Copyright (c) All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000..0741db7 --- /dev/null +++ b/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,26 @@ +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/LICENSES/FSFAP.txt b/LICENSES/FSFAP.txt new file mode 100644 index 0000000..32bc8a8 --- /dev/null +++ b/LICENSES/FSFAP.txt @@ -0,0 +1 @@ +Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. This file is offered as-is, without any warranty. diff --git a/LICENSES/GPL-2.0-or-later.txt b/LICENSES/GPL-2.0-or-later.txt new file mode 100644 index 0000000..3b6070f --- /dev/null +++ b/LICENSES/GPL-2.0-or-later.txt @@ -0,0 +1,311 @@ +GNU GENERAL PUBLIC LICENSE +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software +is covered by the GNU Lesser General Public License instead.) You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must give the recipients all the rights that you have. You +must make sure that they, too, receive or can get the source code. And you +must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If +the software is modified by someone else and passed on, we want its recipients +to know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will individually +obtain patent licenses, in effect making the program proprietary. To prevent +this, we have made it clear that any patent must be licensed for everyone's +free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms +of this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or translated +into another language. (Hereinafter, translation is included without limitation +in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running the Program +is not restricted, and the output from the Program is covered only if its +contents constitute a work based on the Program (independent of having been +made by running the Program). Whether that is true depends on what the Program +does. + +1. You may copy and distribute verbatim copies of the Program's source code +as you receive it, in any medium, provided that you conspicuously and appropriately +publish on each copy an appropriate copyright notice and disclaimer of warranty; +keep intact all the notices that refer to this License and to the absence +of any warranty; and give any other recipients of the Program a copy of this +License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, +thus forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + +a) You must cause the modified files to carry prominent notices stating that +you changed the files and the date of any change. + +b) You must cause any work that you distribute or publish, that in whole or +in part contains or is derived from the Program or any part thereof, to be +licensed as a whole at no charge to all third parties under the terms of this +License. + +c) If the modified program normally reads commands interactively when run, +you must cause it, when started running for such interactive use in the most +ordinary way, to print or display an announcement including an appropriate +copyright notice and a notice that there is no warranty (or else, saying that +you provide a warranty) and that users may redistribute the program under +these conditions, and telling the user how to view a copy of this License. +(Exception: if the Program itself is interactive but does not normally print +such an announcement, your work based on the Program is not required to print +an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Program, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Program. + +In addition, mere aggregation of another work not based on the Program with +the Program (or with a work based on the Program) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under Section +2) in object code or executable form under the terms of Sections 1 and 2 above +provided that you also do one of the following: + +a) Accompany it with the complete corresponding machine-readable source code, +which must be distributed under the terms of Sections 1 and 2 above on a medium +customarily used for software interchange; or, + +b) Accompany it with a written offer, valid for at least three years, to give +any third party, for a charge no more than your cost of physically performing +source distribution, a complete machine-readable copy of the corresponding +source code, to be distributed under the terms of Sections 1 and 2 above on +a medium customarily used for software interchange; or, + +c) Accompany it with the information you received as to the offer to distribute +corresponding source code. (This alternative is allowed only for noncommercial +distribution and only if you received the program in object code or executable +form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +If distribution of executable or object code is made by offering access to +copy from a designated place, then offering equivalent access to copy the +source code from the same place counts as distribution of the source code, +even though third parties are not compelled to copy the source along with +the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except +as expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses terminated +so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Program or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Program +(or any work based on the Program), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor +to copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of +the rights granted herein. You are not responsible for enforcing compliance +by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Program at all. For example, if a +patent license would not permit royalty-free redistribution of the Program +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system, which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Program under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of +the General Public License from time to time. Such new versions will be similar +in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Program does not specify a version number of this License, you may choose +any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing and reuse +of software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + +one line to give the program's name and an idea of what it does. Copyright +(C) yyyy name of author + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 51 Franklin +Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how +to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when +it starts in an interactive mode: + +Gnomovision version 69, Copyright (C) year name of author Gnomovision comes +with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, +and you are welcome to redistribute it under certain conditions; type `show +c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than `show w' and `show c'; they could even be mouse-clicks +or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' +(which makes passes at compilers) written by James Hacker. + +signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice diff --git a/LICENSES/LGPL-2.0-or-later.txt b/LICENSES/LGPL-2.0-or-later.txt new file mode 100644 index 0000000..5c96471 --- /dev/null +++ b/LICENSES/LGPL-2.0-or-later.txt @@ -0,0 +1,446 @@ +GNU LIBRARY GENERAL PUBLIC LICENSE + +Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. + +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the library GPL. It is numbered 2 because +it goes with version 2 of the ordinary GPL.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Library General Public License, applies to some specially +designated Free Software Foundation software, and to any other libraries whose +authors decide to use it. You can use it for your libraries, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library, or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +a program with the library, you must provide complete object files to the +recipients so that they can relink them with the library, after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +Our method of protecting your rights has two steps: (1) copyright the library, +and (2) offer you this license which gives you legal permission to copy, distribute +and/or modify the library. + +Also, for each distributor's protection, we want to make certain that everyone +understands that there is no warranty for this free library. If the library +is modified by someone else and passed on, we want its recipients to know +that what they have is not the original version, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that companies distributing free software will individually +obtain patent licenses, thus in effect transforming the program into proprietary +software. To prevent this, we have made it clear that any patent must be licensed +for everyone's free use or not licensed at all. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License, which was designed for utility programs. This license, +the GNU Library General Public License, applies to certain designated libraries. +This license is quite different from the ordinary one; be sure to read it +in full, and don't assume that anything in it is the same as in the ordinary +license. + +The reason we have a separate public license for some libraries is that they +blur the distinction we usually make between modifying or adding to a program +and simply using it. Linking a program with a library, without changing the +library, is in some sense simply using the library, and is analogous to running +a utility program or application program. However, in a textual and legal +sense, the linked executable is a combined work, a derivative of the original +library, and the ordinary General Public License treats it as such. + +Because of this blurred distinction, using the ordinary General Public License +for libraries did not effectively promote software sharing, because most developers +did not use the libraries. We concluded that weaker conditions might promote +sharing better. + +However, unrestricted linking of non-free programs would deprive the users +of those programs of all benefit from the free status of the libraries themselves. +This Library General Public License is intended to permit developers of non-free +programs to use free libraries, while preserving your freedom as a user of +such programs to change the free libraries that are incorporated in them. +(We have not seen how to achieve this as regards changes in header files, +but we have achieved it as regards changes in the actual functions of the +Library.) The hope is that this will lead to faster development of free libraries. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, while the latter only works together with the library. + +Note that it is possible for a library to be covered by the ordinary General +Public License rather than by this special one. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library which contains a +notice placed by the copyright holder or other authorized party saying it +may be distributed under the terms of this Library General Public License +(also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also compile or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +c) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +d) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the source code distributed need +not include anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the operating +system on which the executable runs, unless that component itself accompanies +the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties to this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Library General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + +one line to give the library's name and an idea of what it does. + +Copyright (C) year name of author + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Library General Public License as published by the Free +Software Foundation; either version 2 of the License, or (at your option) +any later version. + +This library is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more +details. + +You should have received a copy of the GNU Library General Public License +along with this library; if not, write to the Free Software Foundation, Inc., +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in + +the library `Frob' (a library for tweaking knobs) written + +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 + +Ty Coon, President of Vice + +That's all there is to it! diff --git a/LICENSES/LGPL-2.1-only.txt b/LICENSES/LGPL-2.1-only.txt new file mode 100644 index 0000000..130dffb --- /dev/null +++ b/LICENSES/LGPL-2.1-only.txt @@ -0,0 +1,467 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. + +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as the +successor of the GNU Library Public License, version 2, hence the version +number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Lesser General Public License, applies to some specially +designated software packages--typically libraries--of the Free Software Foundation +and other authors who decide to use it. You can use it too, but we suggest +you first think carefully about whether this license or the ordinary General +Public License is the better strategy to use in any particular case, based +on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. +Our General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish); that you receive source code or can get it if you want it; that you +can change the software and use pieces of it in new free programs; and that +you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors +to deny you these rights or to ask you to surrender these rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +other code with the library, you must provide complete object files to the +recipients, so that they can relink them with the library after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, +and (2) we offer you this license, which gives you legal permission to copy, +distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no +warranty for the free library. Also, if the library is modified by someone +else and passed on, the recipients should know that what they have is not +the original version, so that the original author's reputation will not be +affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free +program. We wish to make sure that a company cannot effectively restrict the +users of a free program by obtaining a restrictive license from a patent holder. +Therefore, we insist that any patent license obtained for a version of the +library must be consistent with the full freedom of use specified in this +license. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License. This license, the GNU Lesser General Public License, +applies to certain designated libraries, and is quite different from the ordinary +General Public License. We use this license for certain libraries in order +to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared +library, the combination of the two is legally speaking a combined work, a +derivative of the original library. The ordinary General Public License therefore +permits such linking only if the entire combination fits its criteria of freedom. +The Lesser General Public License permits more lax criteria for linking other +code with the library. + +We call this license the "Lesser" General Public License because it does Less +to protect the user's freedom than the ordinary General Public License. It +also provides other free software developers Less of an advantage over competing +non-free programs. These disadvantages are the reason we use the ordinary +General Public License for many libraries. However, the Lesser license provides +advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the +widest possible use of a certain library, so that it becomes a de-facto standard. +To achieve this, non-free programs must be allowed to use the library. A more +frequent case is that a free library does the same job as widely used non-free +libraries. In this case, there is little to gain by limiting the free library +to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs +enables a greater number of people to use a large body of free software. For +example, permission to use the GNU C Library in non-free programs enables +many more people to use the whole GNU operating system, as well as its variant, +the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' +freedom, it does ensure that the user of a program that is linked with the +Library has the freedom and the wherewithal to run that program using a modified +version of the Library. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, whereas the latter must be combined with the library in +order to run. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program +which contains a notice placed by the copyright holder or other authorized +party saying it may be distributed under the terms of this Lesser General +Public License (also called "this License"). Each licensee is addressed as +"you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Use a suitable shared library mechanism for linking with the Library. A +suitable mechanism is one that (1) uses at run time a copy of the library +already present on the user's computer system, rather than copying library +functions into the executable, and (2) will operate properly with a modified +version of the library, if the user installs one, as long as the modified +version is interface-compatible with the version that the work was made with. + +c) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +d) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +e) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the materials to be distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Lesser General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + +< one line to give the library's name and an idea of what it does. > + +Copyright (C) < year > < name of author > + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Lesser General Public License as published by the Free +Software Foundation; either version 2.1 of the License, or (at your option) +any later version. + +This library is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. + +You should have received a copy of the GNU Lesser General Public License along +with this library; if not, write to the Free Software Foundation, Inc., 51 +Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information +on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in + +the library `Frob' (a library for tweaking knobs) written + +by James Random Hacker. + +< signature of Ty Coon > , 1 April 1990 + +Ty Coon, President of Vice + +That's all there is to it! diff --git a/LICENSES/LGPL-2.1-or-later.txt b/LICENSES/LGPL-2.1-or-later.txt new file mode 100644 index 0000000..aaaba16 --- /dev/null +++ b/LICENSES/LGPL-2.1-or-later.txt @@ -0,0 +1,462 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as +the successor of the GNU Library Public License, version 2, hence the version +number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Lesser General Public License, applies to some specially +designated software packages--typically libraries--of the Free Software Foundation +and other authors who decide to use it. You can use it too, but we suggest +you first think carefully about whether this license or the ordinary General +Public License is the better strategy to use in any particular case, based +on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. +Our General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish); that you receive source code or can get it if you want it; that you +can change the software and use pieces of it in new free programs; and that +you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors +to deny you these rights or to ask you to surrender these rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +other code with the library, you must provide complete object files to the +recipients, so that they can relink them with the library after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, +and (2) we offer you this license, which gives you legal permission to copy, +distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no +warranty for the free library. Also, if the library is modified by someone +else and passed on, the recipients should know that what they have is not +the original version, so that the original author's reputation will not be +affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free +program. We wish to make sure that a company cannot effectively restrict the +users of a free program by obtaining a restrictive license from a patent holder. +Therefore, we insist that any patent license obtained for a version of the +library must be consistent with the full freedom of use specified in this +license. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License. This license, the GNU Lesser General Public License, +applies to certain designated libraries, and is quite different from the ordinary +General Public License. We use this license for certain libraries in order +to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared +library, the combination of the two is legally speaking a combined work, a +derivative of the original library. The ordinary General Public License therefore +permits such linking only if the entire combination fits its criteria of freedom. +The Lesser General Public License permits more lax criteria for linking other +code with the library. + +We call this license the "Lesser" General Public License because it does Less +to protect the user's freedom than the ordinary General Public License. It +also provides other free software developers Less of an advantage over competing +non-free programs. These disadvantages are the reason we use the ordinary +General Public License for many libraries. However, the Lesser license provides +advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the +widest possible use of a certain library, so that it becomes a de-facto standard. +To achieve this, non-free programs must be allowed to use the library. A more +frequent case is that a free library does the same job as widely used non-free +libraries. In this case, there is little to gain by limiting the free library +to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs +enables a greater number of people to use a large body of free software. For +example, permission to use the GNU C Library in non-free programs enables +many more people to use the whole GNU operating system, as well as its variant, +the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' +freedom, it does ensure that the user of a program that is linked with the +Library has the freedom and the wherewithal to run that program using a modified +version of the Library. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, whereas the latter must be combined with the library in +order to run. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program +which contains a notice placed by the copyright holder or other authorized +party saying it may be distributed under the terms of this Lesser General +Public License (also called "this License"). Each licensee is addressed as +"you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Use a suitable shared library mechanism for linking with the Library. A +suitable mechanism is one that (1) uses at run time a copy of the library +already present on the user's computer system, rather than copying library +functions into the executable, and (2) will operate properly with a modified +version of the library, if the user installs one, as long as the modified +version is interface-compatible with the version that the work was made with. + +c) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +d) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +e) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the materials to be distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Lesser General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Lesser General Public License as published by the Free +Software Foundation; either version 2.1 of the License, or (at your option) +any later version. + +This library is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. + +You should have received a copy of the GNU Lesser General Public License along +with this library; if not, write to the Free Software Foundation, Inc., 51 +Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information +on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice +That's all there is to it! diff --git a/LICENSES/LGPL-3.0-only.txt b/LICENSES/LGPL-3.0-only.txt new file mode 100644 index 0000000..bd405af --- /dev/null +++ b/LICENSES/LGPL-3.0-only.txt @@ -0,0 +1,163 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms +and conditions of version 3 of the GNU General Public License, supplemented +by the additional permissions listed below. + + 0. Additional Definitions. + + + +As used herein, "this License" refers to version 3 of the GNU Lesser General +Public License, and the "GNU GPL" refers to version 3 of the GNU General Public +License. + + + +"The Library" refers to a covered work governed by this License, other than +an Application or a Combined Work as defined below. + + + +An "Application" is any work that makes use of an interface provided by the +Library, but which is not otherwise based on the Library. Defining a subclass +of a class defined by the Library is deemed a mode of using an interface provided +by the Library. + + + +A "Combined Work" is a work produced by combining or linking an Application +with the Library. The particular version of the Library with which the Combined +Work was made is also called the "Linked Version". + + + +The "Minimal Corresponding Source" for a Combined Work means the Corresponding +Source for the Combined Work, excluding any source code for portions of the +Combined Work that, considered in isolation, are based on the Application, +and not on the Linked Version. + + + +The "Corresponding Application Code" for a Combined Work means the object +code and/or source code for the Application, including any data and utility +programs needed for reproducing the Combined Work from the Application, but +excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + +You may convey a covered work under sections 3 and 4 of this License without +being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + +If you modify a copy of the Library, and, in your modifications, a facility +refers to a function or data to be supplied by an Application that uses the +facility (other than as an argument passed when the facility is invoked), +then you may convey a copy of the modified version: + +a) under this License, provided that you make a good faith effort to ensure +that, in the event an Application does not supply the function or data, the +facility still operates, and performs whatever part of its purpose remains +meaningful, or + +b) under the GNU GPL, with none of the additional permissions of this License +applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + +The object code form of an Application may incorporate material from a header +file that is part of the Library. You may convey such object code under terms +of your choice, provided that, if the incorporated material is not limited +to numerical parameters, data structure layouts and accessors, or small macros, +inline functions and templates (ten or fewer lines in length), you do both +of the following: + +a) Give prominent notice with each copy of the object code that the Library +is used in it and that the Library and its use are covered by this License. + +b) Accompany the object code with a copy of the GNU GPL and this license document. + + 4. Combined Works. + +You may convey a Combined Work under terms of your choice that, taken together, +effectively do not restrict modification of the portions of the Library contained +in the Combined Work and reverse engineering for debugging such modifications, +if you also do each of the following: + +a) Give prominent notice with each copy of the Combined Work that the Library +is used in it and that the Library and its use are covered by this License. + +b) Accompany the Combined Work with a copy of the GNU GPL and this license +document. + +c) For a Combined Work that displays copyright notices during execution, include +the copyright notice for the Library among these notices, as well as a reference +directing the user to the copies of the GNU GPL and this license document. + + d) Do one of the following: + +0) Convey the Minimal Corresponding Source under the terms of this License, +and the Corresponding Application Code in a form suitable for, and under terms +that permit, the user to recombine or relink the Application with a modified +version of the Linked Version to produce a modified Combined Work, in the +manner specified by section 6 of the GNU GPL for conveying Corresponding Source. + +1) Use a suitable shared library mechanism for linking with the Library. A +suitable mechanism is one that (a) uses at run time a copy of the Library +already present on the user's computer system, and (b) will operate properly +with a modified version of the Library that is interface-compatible with the +Linked Version. + +e) Provide Installation Information, but only if you would otherwise be required +to provide such information under section 6 of the GNU GPL, and only to the +extent that such information is necessary to install and execute a modified +version of the Combined Work produced by recombining or relinking the Application +with a modified version of the Linked Version. (If you use option 4d0, the +Installation Information must accompany the Minimal Corresponding Source and +Corresponding Application Code. If you use option 4d1, you must provide the +Installation Information in the manner specified by section 6 of the GNU GPL +for conveying Corresponding Source.) + + 5. Combined Libraries. + +You may place library facilities that are a work based on the Library side +by side in a single library together with other library facilities that are +not Applications and are not covered by this License, and convey such a combined +library under terms of your choice, if you do both of the following: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities, conveyed under the +terms of this License. + +b) Give prominent notice with the combined library that part of it is a work +based on the Library, and explaining where to find the accompanying uncombined +form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + +The Free Software Foundation may publish revised and/or new versions of the +GNU Lesser General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you +received it specifies that a certain numbered version of the GNU Lesser General +Public License "or any later version" applies to it, you have the option of +following the terms and conditions either of that published version or of +any later version published by the Free Software Foundation. If the Library +as you received it does not specify a version number of the GNU Lesser General +Public License, you may choose any version of the GNU Lesser General Public +License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether +future versions of the GNU Lesser General Public License shall apply, that +proxy's public statement of acceptance of any version is permanent authorization +for you to choose that version for the Library. diff --git a/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt b/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt new file mode 100644 index 0000000..232b3c5 --- /dev/null +++ b/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt @@ -0,0 +1,12 @@ +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the license or (at your option) any later version +that is accepted by the membership of KDE e.V. (or its successor +approved by the membership of KDE e.V.), which shall act as a +proxy as defined in Section 6 of version 3 of the license. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..f0fd20a --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Mainpage.dox b/Mainpage.dox new file mode 100644 index 0000000..3b231fd --- /dev/null +++ b/Mainpage.dox @@ -0,0 +1,216 @@ +/* + * This file is part of Kirigami + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + + +/** \mainpage kirigami + + +\subsection overview Introduction +Kirigami is a set of QtQuick components for building adaptable UIs based on QtQuick Controls 2. + +Its goal is to enable creation of convergent applications that look and feel great on mobile as well as desktop devices and follow the KDE Human Interface Guidelines while being easy to use and not adding many dependencies. + +Kirigami works on a variety of platforms, such as Plasma Mobile, Desktop Linux, Android, MacOS, and Windows. + +It was introduced in KDE Frameworks 5.37 as a Tier-1 KDE Framework. + +\subsection tutorial Tutorial +A tutorial for Kirigami is available on our developer platform. + +It is possible to make short mockups using QtQuick and Kirigami in the QML Online website and briefly test individual QML files using the qml tool. + +A list of additional QML learning resources is available in the Community Wiki. If you are not familiar with QML at all, the QML book should be a good head start. + +If you have any questions about Kirigami, feel free to drop by the Kirigami group on Matrix. + +\subsection components Main Window Components +- \link ApplicationWindow ApplicationWindow \endlink +- \link Action Action \endlink +- \link GlobalDrawer GlobalDrawer \endlink +- \link ContextDrawer ContextDrawer \endlink +- \link OverlayDrawer OverlayDrawer \endlink +- \link Page Page \endlink +- \link ScrollablePage ScrollablePage \endlink +- \link AboutPage AboutPage \endlink +- \link PageRow PageRow \endlink +- \link FormLayout FormLayout \endlink +- \link CardsLayout CardsLayout \endlink +- \link SizeGroup SizeGroup \endlink +- \link Kirigami::Platform::PlatformTheme Theme \endlink +- \link Kirigami::Platform::Units Units \endlink + +\subsection controls Common Kirigami Controls + +- \link Card Card \endlink +- \link org::kde::kirigami::templates::OverlaySheet OverlaySheet \endlink +- \link SwipeListItem SwipeListItem \endlink +- \link Heading Heading \endlink +- \link PlaceholderMessage PlaceholderMessage \endlink +- \link SearchField SearchField \endlink +- \link Dialog Dialog \endlink +- \link NavigationTabBar NavigationTabBar \endlink +- \link Icon Icon \endlink + +\subsection example Minimal Example + +@code +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: root + + width: 500 + height: 400 + + globalDrawer: Kirigami.GlobalDrawer { + actions: [ + Kirigami.Action { + text: "View" + icon.name: "view-list-icons" + Kirigami.Action { + text: "action 1" + } + Kirigami.Action { + text: "action 2" + } + Kirigami.Action { + text: "action 3" + } + }, + Kirigami.Action { + text: "action 3" + }, + Kirigami.Action { + text: "action 4" + } + ] + } + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + } + pageStack.initialPage: mainPageComponent + Component { + id: mainPageComponent + Kirigami.ScrollablePage { + id: page + title: "Hello" + actions { + main: Kirigami.Action { + icon.name: sheet.sheetOpen ? "dialog-cancel" : "document-edit" + onTriggered: { + print("Action button in buttons page clicked"); + sheet.sheetOpen = !sheet.sheetOpen + } + } + left: Kirigami.Action { + icon.name: "go-previous" + onTriggered: { + print("Left action triggered") + } + } + right: Kirigami.Action { + icon.name: "go-next" + onTriggered: { + print("Right action triggered") + } + } + contextualActions: [ + Kirigami.Action { + text:"Action for buttons" + icon.name: "bookmarks" + onTriggered: print("Action 1 clicked") + }, + Kirigami.Action { + text:"Action 2" + icon.name: "folder" + enabled: false + }, + Kirigami.Action { + text: "Action for Sheet" + visible: sheet.sheetOpen + } + ] + } + Kirigami.OverlaySheet { + id: sheet + onSheetOpenChanged: page.actions.main.checked = sheetOpen + QQC2.Label { + wrapMode: Text.WordWrap + text: "Lorem ipsum dolor sit amet" + } + } + //Page contents... + Rectangle { + anchors.fill: parent + color: "lightblue" + } + } + } +} +@endcode + +@image html MinimalExample.webp + +\subsection deployment Deployment +CMake is used for both building Kirigami and projects using it, allowing for several configurations depending on how the deployment needs to be done. + +Kirigami can be built in two ways: both as a module or statically linked to the application, leading to four combinations: + +- Kirigami built as a module with CMake +- Kirigami statically built with CMake (needed to link statically from applications built with CMake) + +The simplest and recommended way to use Kirigami is to just use the packages provided by your Linux distribution, or build it as a module and deploy it together with the main application. + +For example, when building an application on Android with CMake, if Kirigami for Android is built and installed in the same temporary directory before the application, the create-apk step of the application will include the Kirigami files as well in the APK. + +Statically linked Kirigami will be used only on Android, while on desktop systems it will use the version provided by the distribution. Which platforms use the static version and which use the dynamic one can be freely adjusted. + +The application needs to have a folder called "3rdparty" containing clones of two KDE repositories: kirigami and breeze-icons (available at git://anongit.kde.org/kirigami.git and git://anongit.kde.org/breeze-icons.git). + +The main.cpp file should then have something like: + +@code +#include +#include +#ifdef Q_OS_ANDROID +#include "./3rdparty/kirigami/src/kirigamiplugin.h" +#endif + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + +#ifdef Q_OS_ANDROID + KirigamiPlugin::getInstance().registerTypes(&engine); +#endif + ... +} +@endcode + +@authors +Marco Martin \
+Sebastian Kuegler \
+Aleix Pol Gonzalez \
+Dirk Hohndel \
+ +@maintainers +Marco Martin \ + +@licenses +@lgpl + +*/ + + +// DOXYGEN_SET_RECURSIVE = YES +// DOXYGEN_SET_EXCLUDE_PATTERNS += *_p.h */private/* */examples/* */doc/* */styles/* +// DOXYGEN_SET_PROJECT_NAME = Kirigami +// vim:ts=4:sw=4:expandtab:filetype=doxygen diff --git a/README.md b/README.md new file mode 100644 index 0000000..d43528f --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# Kirigami + +QtQuick plugins to build user interfaces following the [KDE Human Interface Guidelines](https://develop.kde.org/hig/). + +## Introduction + +Kirigami is a set of [QtQuick](https://doc.qt.io/qt-6/qtquick-index.html) components for building adaptable user interfaces based on [QtQuick Controls 2](https://doc.qt.io/qt-6/qtquickcontrols-index.html). Kirigami makes it easy to create applications that look and feel great on [Plasma Mobile](https://plasma-mobile.org/), Desktop Linux, Android, MacOS, and Windows. + +The API can be found in the [KDE API Reference website](https://api.kde.org/frameworks/kirigami/html/index.html) and a Kirigami tutorial is available in the [KDE Developer Platform](https://develop.kde.org/docs/getting-started/kirigami/). + +We also provide [Kirigami Gallery](https://invent.kde.org/sdk/kirigami-gallery) to showcase most Kirigami components. + +## Building Kirigami + +After installing `extra-cmake-modules` (ECM) and the necessary Qt6 development libraries, run: + +```bash +git clone https://invent.kde.org/frameworks/kirigami.git +cd kirigami +cmake -B build/ -DCMAKE_INSTALL_PREFIX=/path/where/kirigami/will/be/installed +cmake --build build/ +cmake --install build/ +``` + +If you compiled and installed ECM yourself, you will need to add it to your PATH to compile Kirigami with it, as ECM does not provide its own `prefix.sh` file: + +```bash +PATH=/path/to/the/ecm/installation/usr/ cmake -B build/ -DCMAKE_INSTALL_PREFIX=/path/where/kirigami/will/be/installed +cmake --build build/ +cmake --install build/ +``` + +Alternatively, we recommend you use [kdesrc-build](https://community.kde.org/Get_Involved/development#Setting_up_the_development_environment) to build extra-cmake-modules and Kirigami together. + +The provided Kirigami example can be built and run with: + +```bash +cmake -B build/ -DBUILD_EXAMPLES=ON +cmake --build build/ +./build/bin/applicationitemapp +``` + +And the remaining examples containing only single QML files in the `examples/` folder can be viewed using `qml ` or `qmlscene `. + +# Using a self-compiled Kirigami in your application + +To compile your application and link a self-compiled build of Kirigami to it, run: + +```bash +source path/to/kirigami/build/prefix.sh +``` + +And then compile your application as usual. + +# Build your Android application and ship it with Kirigami + +1) Build Kirigami + +You will need to compile Qt for Android or use the [Qt Installer](https://www.qt.io/download-open-source) to install it, in addition to the Android SDK and NDK. After that, run: + +```bash +cmake -B build/ \ + -DCMAKE_TOOLCHAIN_FILE=/usr/share/ECM/toolchain/Android.cmake \ + -DCMAKE_PREFIX_PATH=/path/to/Qt5.15.9/5.15/android_armv7/ \ + -DCMAKE_INSTALL_PREFIX=/path/where/kirigami/will/be/installed/ \ + -DECM_DIR=/usr/share/ECM/cmake + +cmake --build build/ +cmake --install build/ +``` + +2) Build your application + +This guide assumes that you build your application with CMake and use [Extra CMake Modules (ECM)](https://api.kde.org/ecm/) from KDE frameworks. + +Replace `$yourapp` with the actual name of your application: + +```bash +cmake -B build/ \ + -DCMAKE_TOOLCHAIN_FILE=/usr/share/ECM/toolchain/Android.cmake \ + -DQTANDROID_EXPORTED_TARGET=$yourapp \ + -DANDROID_APK_DIR=../path/to/yourapp/ \ + -DCMAKE_PREFIX_PATH=/path/to/Qt5.15.9/5.15/android_armv7/ \ + -DCMAKE_INSTALL_PREFIX=/path/where/yourapp/will/be/installed/ + +cmake --build build/ +cmake --install build/ +cmake --build build/ --target create-apk-$yourapp +``` + +Note: `-DCMAKE_INSTALL_PREFIX` directory should be the same as where Kirigami was installed, since you need to create an apk package that contains both the Kirigami build and the build of your application. diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt new file mode 100644 index 0000000..c148961 --- /dev/null +++ b/autotests/CMakeLists.txt @@ -0,0 +1,87 @@ +if(NOT TARGET Qt6::QuickTest) + message(STATUS "Qt6QuickTest not found, autotests will not be built.") + return() +endif() + +add_executable(qmltest qmltest.cpp actiondata.cpp) +qt_add_qml_module(qmltest URI KirigamiTestUtils) +target_link_libraries(qmltest PRIVATE Qt6::Qml Qt6::QuickTest Kirigami) +if (NOT QT6_IS_SHARED_LIBS_BUILD OR NOT BUILD_SHARED_LIBS) + qt6_import_qml_plugins(qmltest) +endif() + +macro(kirigami_add_tests) + if (WIN32) + set(_extra_args -platform offscreen) + endif() + + if (BUILD_SHARED_LIBS) + set(_extra_args ${_extra_args} -import ${CMAKE_BINARY_DIR}/bin) + endif() + + foreach(test ${ARGV}) + add_test(NAME ${test} + COMMAND qmltest + ${_extra_args} + -input ${test} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endforeach() +endmacro() + +kirigami_add_tests( + tst_action.qml + tst_actiontoolbar.qml + tst_card.qml + tst_colorutils.qml + tst_columnview.qml + tst_delegates.qml + tst_dialogs.qml + tst_formlayout.qml + tst_globaldrawer.qml + tst_headerfooterlayout.qml + tst_icon.qml + tst_ImageColors.qml + tst_inlinemessage.qml + tst_inlineviewheader.qml + tst_keynavigation.qml + tst_listskeynavigation.qml + tst_menudialog.qml + tst_mnemonicdata.qml + tst_navigationtabbar.qml + tst_overlaysheet.qml + tst_overlayzstacking.qml + tst_padding.qml + tst_pagerow.qml + tst_pageStackAttached.qml + tst_placeholdermessage.qml + tst_sceneposition.qml + tst_scrollablepage.qml + tst_spellcheck.qml + tst_theme.qml + + mobile/tst_pagerow.qml + + pagepool/tst_layers.qml + pagepool/tst_pagepool.qml + + wheelhandler/tst_filterMouseEvents.qml + wheelhandler/tst_invokables.qml + wheelhandler/tst_onWheel.qml + wheelhandler/tst_scrolling.qml +) + +set_tests_properties( + tst_actiontoolbar.qml + tst_theme.qml + + PROPERTIES + ENVIRONMENT "QT_QUICK_CONTROLS_STYLE=Basic;KIRIGAMI_FORCE_STYLE=1" +) + +set_tests_properties( + mobile/tst_pagerow.qml + + PROPERTIES + ENVIRONMENT "QT_QUICK_CONTROLS_MOBILE=1" +) diff --git a/autotests/actiondata.cpp b/autotests/actiondata.cpp new file mode 100644 index 0000000..f78dc74 --- /dev/null +++ b/autotests/actiondata.cpp @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "actiondata.h" + +using namespace Qt::StringLiterals; + +ActionData::ActionData(QObject *parent) + : QObject(parent) + , m_enabledAction(new QAction(this)) + , m_disabledAction(new QAction(this)) +{ + m_enabledAction->setText(u"Enabled Action"_s); + + m_disabledAction->setText(u"Disabled Action"_s); + m_disabledAction->setEnabled(false); +} + +QAction *ActionData::enabledAction() const +{ + return m_enabledAction; +} + +QAction *ActionData::disabledAction() const +{ + return m_disabledAction; +} + +#include "moc_actiondata.cpp" diff --git a/autotests/actiondata.h b/autotests/actiondata.h new file mode 100644 index 0000000..8113622 --- /dev/null +++ b/autotests/actiondata.h @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2024 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include +#include + +class ActionData : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(QAction *enabledAction READ enabledAction CONSTANT) + Q_PROPERTY(QAction *disabledAction READ disabledAction CONSTANT) + +public: + explicit ActionData(QObject *parent = nullptr); + + QAction *enabledAction() const; + QAction *disabledAction() const; + +private: + QAction *const m_enabledAction; + QAction *const m_disabledAction; +}; diff --git a/autotests/mobile/tst_pagerow.qml b/autotests/mobile/tst_pagerow.qml new file mode 100644 index 0000000..7ea7f6e --- /dev/null +++ b/autotests/mobile/tst_pagerow.qml @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtTest +import org.kde.kirigami as Kirigami + +TestCase { + id: testCase + + width: 400 + height: 400 + name: "MobilePageRowTest" + visible: false + + Component { + id: applicationComponent + Kirigami.ApplicationWindow { + // Mobile pagerow logic branches at 40 gridUnits boundary + width: Kirigami.Units.gridUnit * 30 + height: 400 + } + } + + Component { + id: pageComponent + Kirigami.Page { + id: page + property alias closeButton: closeButton + title: "TestPageComponent" + QQC2.Button { + id: closeButton + anchors.centerIn: parent + objectName: "CloseDialogButton" + text: "Close" + onClicked: page.closeDialog(); + } + } + } + + // The following methods are adaptation of QtTest internals + + function waitForWindowActive(window: Window) { + tryVerify(() => window.active); + } + + function ensureWindowShown(window: Window) { + window.requestActivate(); + waitForWindowActive(window); + wait(0); + } + + function init() { + verify(Kirigami.Settings.isMobile); + } + + function test_pushDialogLayer() { + const app = createTemporaryObject(applicationComponent, this); + verify(app); + ensureWindowShown(app); + + verify(app.pageStack.layers instanceof QQC2.StackView); + compare(app.pageStack.layers.depth, 1); + { + const page = app.pageStack.pushDialogLayer(pageComponent); + verify(page instanceof Kirigami.Page); + compare(page.title, "TestPageComponent"); + // Wait for it to finish animating + tryVerify(() => !app.pageStack.layers.busy); + compare(app.pageStack.layers.depth, 2); + mouseClick(page.closeButton); + tryVerify(() => !app.pageStack.layers.busy); + compare(app.pageStack.layers.depth, 1); + } + app.width = Kirigami.Units.gridUnit * 50; + { + const page = app.pageStack.pushDialogLayer(pageComponent); + verify(page instanceof Kirigami.Page); + compare(page.title, "TestPageComponent"); + verify(!app.pageStack.layers.busy); + compare(app.pageStack.layers.depth, 1); + mouseClick(page.closeButton); + } + } +} diff --git a/autotests/pagepool/TestPage.qml b/autotests/pagepool/TestPage.qml new file mode 100644 index 0000000..4eacef3 --- /dev/null +++ b/autotests/pagepool/TestPage.qml @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2020 Mason McParlane + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +Kirigami.Page { + title: qsTr("INITIAL TITLE") +} diff --git a/autotests/pagepool/tst_layers.qml b/autotests/pagepool/tst_layers.qml new file mode 100644 index 0000000..39842b1 --- /dev/null +++ b/autotests/pagepool/tst_layers.qml @@ -0,0 +1,311 @@ +/* + * SPDX-FileCopyrightText: 2020 Mason McParlane + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Window +import org.kde.kirigami as Kirigami +import QtTest + +TestCase { + id: testCase + width: 400 + height: 400 + name: "PagePoolWithLayers" + when: windowShown + + function initTestCase() { + mainWindow.show() + } + + function cleanupTestCase() { + mainWindow.close() + } + + Kirigami.ApplicationWindow { + id: mainWindow + width: 480 + height: 360 + } + + Kirigami.PagePool { + id: pool + } + + SignalSpy { + id: stackSpy + target: mainWindow.pageStack + signalName: "onCurrentItemChanged" + } + + SignalSpy { + id: layerSpy + target: mainWindow.pageStack.layers + signalName: "onCurrentItemChanged" + } + + + function init() { + pool.clear() + mainWindow.pageStack.layers.clear() + compare(mainWindow.pageStack.layers.depth, 1) + mainWindow.pageStack.clear() + + for (var spy of [stackSpy, layerSpy, checkSpy_A, checkSpy_B, checkSpy_C, checkSpy_D, checkSpy_E]) { + spy.clear() + } + + // Give mainWindow a bit of room to breathe so it can process item + // deletion and other delayed signals. + wait(50) + } + + ActionGroup { + id: group + exclusive: false + + Kirigami.PagePoolAction { + id: stackPageA + objectName: "stackPageA" + pagePool: pool + pageStack: mainWindow.pageStack + page: "TestPage.qml?page=A" + initialProperties: { return {title: "A", objectName: "Page A" } } + } + + Kirigami.PagePoolAction { + id: stackPageB + objectName: "stackPageB" + pagePool: pool + pageStack: mainWindow.pageStack + page: "TestPage.qml?page=B" + initialProperties: { return {title: "B", objectName: "Page B" } } + } + + Kirigami.PagePoolAction { + id: layerPageC + objectName: "layerPageC" + pagePool: pool + pageStack: mainWindow.pageStack + useLayers: true + page: "TestPage.qml?page=C" + initialProperties: { return {title: "C", objectName: "Page C" } } + } + + Kirigami.PagePoolAction { + id: layerPageD + objectName: "layerPageD" + pagePool: pool + pageStack: mainWindow.pageStack + useLayers: true + page: "TestPage.qml?page=D" + initialProperties: { return {title: "D", objectName: "Page D" } } + } + + Kirigami.PagePoolAction { + id: stackPageE + objectName: "stackPageE" + pagePool: pool + pageStack: mainWindow.pageStack + page: "TestPage.qml?page=E" + initialProperties: { return {title: "E", objectName: "Page E" } } + } + } + + function tapBack () { + mouseClick(mainWindow, 10, 10) + } + + function test_pushLayerBackButtonPushAgain() { + var stack = mainWindow.pageStack + var layers = stack.layers + + function pushA() { + stackPageA.trigger() + compare(stack.currentItem, pool.lastLoadedItem) + } + + function pushC () { + layerPageC.trigger() + compare(layers.currentItem, pool.lastLoadedItem) + } + + function pushD () { + layerPageD.trigger() + compare(layers.currentItem, pool.lastLoadedItem) + } + + compare(stackSpy.count, 0) + pushA() + compare(stackSpy.count, 1) + compare(layerSpy.count, 0) + pushC() + compare(layerSpy.count, 1) + pushD() + compare(layerSpy.count, 2) + compare(stackSpy.count, 1) + tapBack() + compare(layerSpy.count, 3) + pushD() + compare(layerSpy.count, 4) + } + + SignalSpy { + id: checkSpy_A + target: stackPageA + signalName: "onCheckedChanged" + } + + SignalSpy { + id: checkSpy_B + target: stackPageB + signalName: "onCheckedChanged" + } + + SignalSpy { + id: checkSpy_C + target: layerPageC + signalName: "onCheckedChanged" + } + + SignalSpy { + id: checkSpy_D + target: layerPageD + signalName: "onCheckedChanged" + } + + SignalSpy { + id: checkSpy_E + target: stackPageE + signalName: "onCheckedChanged" + } + + function dump_layers(msg = "") { + for (var i = 0; i < mainWindow.pageStack.layers.depth; ++i) { + console.debug(`${msg} ${i}: ${mainWindow.pageStack.layers.get(i)}`) + } + } + + function test_checked() { + var stack = mainWindow.pageStack + var layers = stack.layers + + function testCheck(expected = {}) { + let defaults = { + a: false, b: false, c: false, d: false, e: false + } + let actual = Object.assign({}, defaults, expected) + let pages = {a: stackPageA, b: stackPageB, c: layerPageC, d: layerPageD, e: stackPageE} + + for (const prop in actual) { + compare(pages[prop].checked, actual[prop], + `${pages[prop]} should ${actual[prop] ? 'be checked' : 'not be checked'}`) + } + } + + testCheck() + + compare(stackSpy.count, 0) + compare(layerSpy.count, 0) + compare(checkSpy_A.count, 0) + compare(checkSpy_B.count, 0) + compare(checkSpy_C.count, 0) + compare(checkSpy_D.count, 0) + compare(checkSpy_E.count, 0) + + stackPageA.trigger() + compare(checkSpy_A.count, 1) + testCheck({a:true}) + compare(stack.currentItem, stackPageA.pageItem()) + + stackPageB.trigger() + compare(checkSpy_A.count, 2) + compare(checkSpy_B.count, 3) + testCheck({b:true}) + compare(stack.currentItem, stackPageB.pageItem()) + + layerPageC.trigger() + testCheck({b:true, c:true}) + compare(checkSpy_C.count, 1) + compare(stack.currentItem, stackPageB.pageItem()) + compare(layers.currentItem, layerPageC.pageItem()) + compare(layerPageC.layerContainsPage(), true) + + layerPageD.trigger() + compare(stack.currentItem, stackPageB.pageItem()) + compare(layers.currentItem, layerPageD.pageItem()) + testCheck({b:true, c:true, d:true}) + + stackPageE.basePage = stack.currentItem + stackPageE.trigger() + testCheck({b:true, e:true}) + compare(stack.currentItem, stackPageE.pageItem()) + verify(!(layers.currentItem instanceof Page), + `Current item ${layers.currentItem} is a page but all pages should be popped`) + + stackPageA.trigger() + testCheck({a:true}) + compare(stack.currentItem, stackPageA.pageItem()) + verify(!(layers.currentItem instanceof Page), + `Current item ${layers.currentItem} is a page but all pages should be popped`) + + compare(checkSpy_A.count, 5) + compare(checkSpy_B.count, 4) + compare(checkSpy_C.count, 2) + compare(checkSpy_D.count, 2) + compare(checkSpy_E.count, 2) + } + + function test_push_A_C_D_A_popsLayers() { + var stack = mainWindow.pageStack + var layers = stack.layers + + stackPageA.trigger() + compare(stack.currentItem, stackPageA.pageItem()) + + layerPageC.trigger() + compare(layers.currentItem, layerPageC.pageItem()) + + layerPageD.trigger() + compare(layers.currentItem, layerPageD.pageItem()) + + stackPageA.trigger() + compare(stack.currentItem, stackPageA.pageItem()) + verify(!(layers.currentItem instanceof Page), + `Current item ${layers.currentItem} is a page but all pages should be popped`) + } + + function test_push_A_C_D_back_back_C_back_C() { + var stack = mainWindow.pageStack + var layers = stack.layers + + stackPageA.trigger() + layerPageC.trigger() + layerPageD.trigger() + tapBack() + tapBack() + layerPageC.trigger() + tapBack() + layerPageC.trigger() + compare(layers.currentItem, layerPageC.pageItem()) + } + + function test_exclusive_group() { + var stack = mainWindow.pageStack + var layers = stack.layers + + group.exclusive = true + stackPageA.trigger() + compare(stackPageA.checked, true) + compare(layerPageC.checked, false) + layerPageC.trigger() + compare(stackPageA.checked, false) + compare(layerPageC.checked, true) + tapBack() + compare(stackPageA.checked, true) + compare(layerPageC.checked, false) + } +} diff --git a/autotests/pagepool/tst_pagepool.qml b/autotests/pagepool/tst_pagepool.qml new file mode 100644 index 0000000..a498fa4 --- /dev/null +++ b/autotests/pagepool/tst_pagepool.qml @@ -0,0 +1,179 @@ +/* + * SPDX-FileCopyrightText: 2020 Mason McParlane + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Window +import org.kde.kirigami as Kirigami +import QtTest + +TestCase { + id: testCase + width: 400 + height: 400 + name: "PagePool" + + function initTestCase() { + mainWindow.show() + } + + function cleanupTestCase() { + mainWindow.close() + } + + function applicationWindow() { return mainWindow; } + + Kirigami.ApplicationWindow { + id: mainWindow + width: 480 + height: 360 + } + + Kirigami.PagePool { + id: pool + } + + function init() { + mainWindow.pageStack.clear() + pool.clear() + } + + // Queries added to page URLs ensure the PagePool can + // have multiple instances of TestPage.qml + + Kirigami.PagePoolAction { + id: loadPageAction + pagePool: pool + pageStack: mainWindow.pageStack + page: "TestPage.qml?action=loadPageAction" + } + + function test_loadPage () { + var expectedUrl = "TestPage.qml?action=loadPageAction" + compare(mainWindow.pageStack.depth, 0) + loadPageAction.trigger() + compare(mainWindow.pageStack.depth, 1) + verify(pool.lastLoadedUrl.toString().endsWith(expectedUrl)) + compare(mainWindow.pageStack.currentItem.title, "INITIAL TITLE") + } + + Kirigami.PagePoolAction { + id: loadPageActionWithProps + pagePool: pool + pageStack: mainWindow.pageStack + page: "TestPage.qml?action=loadPageActionWithProps" + initialProperties: { + return {title: "NEW TITLE" } + } + } + + function test_loadPageInitialPropertyOverride () { + var expectedUrl = "TestPage.qml?action=loadPageActionWithProps" + compare(mainWindow.pageStack.depth, 0) + loadPageActionWithProps.trigger() + compare(mainWindow.pageStack.depth, 1) + verify(pool.lastLoadedUrl.toString().endsWith(expectedUrl)) + compare(mainWindow.pageStack.currentItem.title, "NEW TITLE") + compare(pool.lastLoadedItem.title, "NEW TITLE") + } + + Kirigami.PagePoolAction { + id: loadPageActionPropsNotObject + pagePool: pool + pageStack: mainWindow.pageStack + page: "TestPage.qml?action=loadPageActionPropsNotObject" + initialProperties: "This is a string not an object..." + } + + function test_loadPageInitialPropertiesWrongType () { + ignoreWarning("initialProperties must be of type object") + var expectedUrl = "TestPage.qml?action=loadPageAction" + compare(mainWindow.pageStack.depth, 0) + loadPageAction.trigger() + loadPageActionPropsNotObject.trigger() + compare(mainWindow.pageStack.depth, 1) + verify(pool.lastLoadedUrl.toString().endsWith(expectedUrl)) + } + + Kirigami.PagePoolAction { + id: loadPageActionPropDoesNotExist + pagePool: pool + pageStack: mainWindow.pageStack + page: "TestPage.qml?action=loadPageActionPropDoesNotExist" + initialProperties: { + return { propDoesNotExist: "PROP-NON-EXISTENT" } + } + } + + function test_loadPageInitialPropertyNotExistFails () { + ignoreWarning(/.*Setting initial properties failed: TestPage does not have a property called propDoesNotExist/) + var expectedUrl = "TestPage.qml?action=loadPageActionPropDoesNotExist" + loadPageActionPropDoesNotExist.trigger() + verify(pool.lastLoadedUrl.toString().endsWith(expectedUrl)) + } + + function test_contains () { + const page = "TestPage.qml?action=contains" + let item = pool.loadPage(page) + verify(item !== null, "valid item returned from loadPage") + verify(pool.contains(page), "pool contains page") + verify(pool.contains(item), "pool contains item") + } + + function test_deletePageByUrl () { + const urlPage = "TestPage.qml?action=deletePageByUrl" + pool.loadPage(urlPage) + verify(pool.contains(urlPage), "pool contains page before deletion") + pool.deletePage(urlPage) + verify(!pool.contains(urlPage), "pool does not contain page after deletion") + } + + function test_deletePageByItem () { + const itemPage = "TestPage.qml?action=deletePageByItem" + let item = pool.loadPage(itemPage) + verify(pool.contains(item), "pool contains item before deletion") + pool.deletePage(item) + verify(!pool.contains(itemPage), "pool does not contain page after deletion") + } + + function test_iterateAndDeleteByItem () { + const pages = [] + for (let i = 1; i <= 5; ++i) { + const page = "TestPage.qml?page=" + i + pool.loadPage(page) + verify(pool.contains(page), "pool contains page " + page) + pages.push(page) + } + const items = Array.prototype.slice.call(pool.items); + compare(items.length, 5, "pool contains 5 items") + for (const item of items) { + const url = pool.urlForPage(item) + const found = pages.find(page => url.toString().endsWith(page)) + verify(found, "pool.items contains page " + found) + pool.deletePage(item) + } + compare(pool.items.length, 0, "all items have been deleted") + } + + function test_iterateAndDeleteByUrl () { + const pages = [] + for (let i = 1; i <= 5; ++i) { + const page = "TestPage.qml?page=" + i + pool.loadPage(page) + verify(pool.contains(page), "pool contains page " + page) + pages.push(page) + } + compare(pool.urls.length, 5, "pool contains 5 urls") + for (const url of pool.urls) { + const found = pages.find(page => url.toString().endsWith(page)) + verify(found, "pool.urls contains page " + found) + } + for (const page of pages) { + pool.deletePage(page) + } + compare(pool.urls.length, 0, "all urls have been deleted") + } +} diff --git a/autotests/qmltest.cpp b/autotests/qmltest.cpp new file mode 100644 index 0000000..c07a73e --- /dev/null +++ b/autotests/qmltest.cpp @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include + +QUICK_TEST_MAIN(Kirigami) diff --git a/autotests/stop-icon.svg b/autotests/stop-icon.svg new file mode 100644 index 0000000..53d549e --- /dev/null +++ b/autotests/stop-icon.svg @@ -0,0 +1,18 @@ + + + + + + diff --git a/autotests/tst_ImageColors.qml b/autotests/tst_ImageColors.qml new file mode 100644 index 0000000..23e6cbe --- /dev/null +++ b/autotests/tst_ImageColors.qml @@ -0,0 +1,133 @@ +/* + * SPDX-FileCopyrightText: 2023 Fushan Wen + * SPDX-FileCopyrightText: 2024 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +import QtQuick +import QtTest +import org.kde.kirigami as Kirigami + +TestCase { + id: testCase + name: "ImageColorsTest" + + width: 400 + height: 400 + visible: true + + when: windowShown + + Component { + id: colorsComponent + Item { + id: root + + readonly property alias colorArea: colorArea + + readonly property Kirigami.ImageColors imageColors: Kirigami.ImageColors { + id: imageColors + source: colorArea + + onPaletteChanged: { + // Check that it can be assigned to + if (palette.length > 0) { + root.swatch = palette[0]; + } + } + } + + readonly property SignalSpy paletteChangedSpy: SignalSpy { + target: imageColors + signalName: "paletteChanged" + } + + // Test that the type can be named + property Kirigami.imageColorsPaletteSwatch swatch + + width: 100 + height: 100 + + Rectangle { + id: colorArea + anchors.fill: parent + color: "transparent" + } + } + } + + Component { + id: invisibleWindowComponent + Window { + visible: false + + readonly property Kirigami.ImageColors imageColors: Kirigami.ImageColors { + source: colorArea + } + + readonly property alias colorArea: colorArea + + Rectangle { + id: colorArea + anchors.fill: parent + color: "transparent" + } + } + } + + Component { + id: noWindowComponent + QtObject { + readonly property Kirigami.ImageColors imageColors: Kirigami.ImageColors { + source: colorArea + } + + readonly property Item colorArea: Rectangle { + id: colorArea + width: 10 + height: 10 + color: "transparent" + } + } + } + + function test_extractColors(): void { + const item = createTemporaryObject(colorsComponent, testCase); + const { colorArea, imageColors, paletteChangedSpy } = item; + + colorArea.color = Qt.rgba(1, 0, 0); + imageColors.update(); + paletteChangedSpy.wait(); + compare(paletteChangedSpy.count, 1); + compare(imageColors.dominant, colorArea.color); + + compare(imageColors.palette.length, 1); + compare(imageColors.palette[0].ratio, 1.0); + compare(imageColors.palette[0].color, colorArea.color); + compare(imageColors.palette[0].contrastColor, "#e6e6e6"); + compare(imageColors.palette[0], item.swatch); + } + + function test_invisibleWindow(): void { + // Do not attempt to grabToImage on an item whose window is invisible. + failOnWarning(/.?/); + + const window = createTemporaryObject(invisibleWindowComponent, this); + const { imageColors, colorArea } = window; + verify(!window.visible); + verify(colorArea.visible); + + imageColors.update(); + } + + function test_noWindow(): void { + // Do not attempt to grabToImage on an item which does not belong to any window. + failOnWarning(/.?/); + + const { imageColors, colorArea } = createTemporaryObject(noWindowComponent, this); + verify(colorArea.visible); + + imageColors.update(); + } +} diff --git a/autotests/tst_action.qml b/autotests/tst_action.qml new file mode 100644 index 0000000..a6fea47 --- /dev/null +++ b/autotests/tst_action.qml @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 Carl Schwan +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import QtTest +import KirigamiTestUtils + +TestCase { + name: "Action" + + Kirigami.Action { + id: normalAction + } + + function test_normal(): void { + compare(normalAction.text, ''); + compare(normalAction.enabled, true); + } + + Kirigami.Action { + id: enabledAction + fromQAction: ActionData.enabledAction + } + + function test_enabledAction(): void { + compare(enabledAction.text, 'Enabled Action'); + compare(enabledAction.enabled, true); + } + + Kirigami.Action { + id: disabledAction + fromQAction: ActionData.disabledAction + } + + function test_disabledAction(): void { + compare(disabledAction.text, 'Disabled Action'); + compare(disabledAction.enabled, false); + } +} \ No newline at end of file diff --git a/autotests/tst_actiontoolbar.qml b/autotests/tst_actiontoolbar.qml new file mode 100644 index 0000000..dba5c95 --- /dev/null +++ b/autotests/tst_actiontoolbar.qml @@ -0,0 +1,342 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls +import QtTest +import org.kde.kirigami as Kirigami + +// TODO: Find a nicer way to handle this +import "../src/controls/private" as KirigamiPrivate + +TestCase { + id: testCase + name: "ActionToolBarTest" + + width: 800 + height: 400 + visible: true + + when: windowShown + + // These buttons are required for getting the right metrics. + // Since ActionToolBar bases all sizing on button sizes, we need to be able + // to verify that layouting does the right thing. + property ToolButton iconButton: KirigamiPrivate.PrivateActionToolButton { + display: Button.IconOnly + action: Kirigami.Action { icon.name: "document-new"; text: "Test Action" } + font.pointSize: 10 + } + property ToolButton textButton: KirigamiPrivate.PrivateActionToolButton { + display: Button.TextOnly + action: Kirigami.Action { icon.name: "document-new"; text: "Test Action" } + font.pointSize: 10 + } + property ToolButton textIconButton: KirigamiPrivate.PrivateActionToolButton { + action: Kirigami.Action { icon.name: "document-new"; text: "Test Action" } + font.pointSize: 10 + } + property TextField textField: TextField { font.pointSize: 10 } + + Component { + id: single + Kirigami.ActionToolBar { + font.pointSize: 10 + actions: [ + Kirigami.Action { icon.name: "document-new"; text: "Test Action" } + ] + } + } + + Component { + id: multiple + Kirigami.ActionToolBar { + font.pointSize: 10 + actions: [ + Kirigami.Action { icon.name: "document-new"; text: "Test Action" }, + Kirigami.Action { icon.name: "document-new"; text: "Test Action" }, + Kirigami.Action { icon.name: "document-new"; text: "Test Action" } + ] + } + } + + Component { + id: iconOnly + Kirigami.ActionToolBar { + display: Button.IconOnly + font.pointSize: 10 + actions: [ + Kirigami.Action { icon.name: "document-new"; text: "Test Action" }, + Kirigami.Action { icon.name: "document-new"; text: "Test Action" }, + Kirigami.Action { icon.name: "document-new"; text: "Test Action" } + ] + } + } + + Component { + id: qtActions + Kirigami.ActionToolBar { + font.pointSize: 10 + actions: [ + Action { icon.name: "document-new"; text: "Test Action" }, + Action { icon.name: "document-new"; text: "Test Action" }, + Action { icon.name: "document-new"; text: "Test Action" } + ] + } + } + + Component { + id: mixed + Kirigami.ActionToolBar { + font.pointSize: 10 + actions: [ + Kirigami.Action { icon.name: "document-new"; text: "Test Action"; displayHint: Kirigami.DisplayHint.IconOnly }, + Kirigami.Action { icon.name: "document-new"; text: "Test Action" }, + Kirigami.Action { icon.name: "document-new"; text: "Test Action"; displayComponent: TextField { } }, + Kirigami.Action { icon.name: "document-new"; text: "Test Action"; displayHint: Kirigami.DisplayHint.AlwaysHide }, + Kirigami.Action { icon.name: "document-new"; text: "Test Action"; displayHint: Kirigami.DisplayHint.KeepVisible } + ] + } + } + + function test_layout_data() { + return [ + // One action + // Full window width, should just display a toolbutton + { tag: "single_full", component: single, width: testCase.width, expected: testCase.textIconButton.width }, + // Small width, should display the overflow button + { tag: "single_min", component: single, width: 50, expected: testCase.iconButton.width }, + // Half window width, should display a single toolbutton + { tag: "single_half", component: single, width: testCase.width / 2, expected: testCase.textIconButton.width }, + // Multiple actions + // Full window width, should display as many buttons as there are actions + { tag: "multi_full", component: multiple, width: testCase.width, + expected: testCase.textIconButton.width * 3 + Kirigami.Units.smallSpacing * 2 }, + // Small width, should display just the overflow button + { tag: "multi_min", component: multiple, width: 50, expected: testCase.iconButton.width }, + // Half window width, should display one action and overflow button + { tag: "multi_small", component: multiple, + width: testCase.textIconButton.width * 2 + testCase.iconButton.width + Kirigami.Units.smallSpacing * 3, + expected: testCase.textIconButton.width * 2 + testCase.iconButton.width + Kirigami.Units.smallSpacing * 2 }, + // Multiple actions, display set to icon only + // Full window width, should display as many icon-only buttons as there are actions + { tag: "icon_full", component: iconOnly, width: testCase.width, + expected: testCase.iconButton.width * 3 + Kirigami.Units.smallSpacing * 2 }, + // Small width, should display just the overflow button + { tag: "icon_min", component: iconOnly, width: 50, expected: testCase.iconButton.width }, + // Quarter window width, should display one icon-only button and the overflow button + { tag: "icon_small", component: iconOnly, width: testCase.iconButton.width * 4, + expected: testCase.iconButton.width * 3 + Kirigami.Units.smallSpacing * 2 }, + // QtQuick Controls actions + // Full window width, should display as many buttons as there are actions + { tag: "qt_full", component: qtActions, width: testCase.width, + expected: testCase.textIconButton.width * 3 + Kirigami.Units.smallSpacing * 2 }, + // Small width, should display just the overflow button + { tag: "qt_min", component: qtActions, width: 50, expected: testCase.iconButton.width }, + // Half window width, should display one action and overflow button + { tag: "qt_small", component: qtActions, + width: testCase.textIconButton.width * 2 + testCase.iconButton.width + Kirigami.Units.smallSpacing * 3, + expected: testCase.textIconButton.width * 2 + testCase.iconButton.width + Kirigami.Units.smallSpacing * 2 }, + // Mix of different display hints, displayComponent and normal actions. + // Full window width, should display everything, but one action is collapsed to icon + { tag: "mixed", component: mixed, width: testCase.width, + expected: testCase.textIconButton.width * 2 + testCase.iconButton.width * 2 + testCase.textField.width + Kirigami.Units.smallSpacing * 4 } + ] + } + + // Test layouting of ActionToolBar + // + // ActionToolBar has some pretty complex behaviour, which generally boils down to it trying + // to fit as many visible actions as possible and placing the hidden ones in an overflow menu. + // This test, along with the data above, verifies that that this behaviour is correct. + function test_layout(data) { + var toolbar = createTemporaryObject(data.component, testCase, {width: data.width}) + + verify(toolbar) + verify(waitForRendering(toolbar)) + + while (toolbar.visibleWidth == 0) { + // The toolbar creates its delegates asynchronously during "idle + // time", this means we need to wait for a bit so the toolbar has + // the time to do that. As long as it has not finished creation, the + // toolbar will have a visibleWidth of 0, so we can use that to + // determine when it is done. + wait(50) + } + + compare(toolbar.visibleWidth, data.expected) + } + + Component { + id: heightMode + + Kirigami.ActionToolBar { + id: heightModeToolBar + font.pointSize: 10 + + property real customHeight: 50 + + actions: [ + Kirigami.Action { + displayComponent: Button { + objectName: "tall" + implicitHeight: heightModeToolBar.customHeight + implicitWidth: 50 + } + }, + Kirigami.Action { + displayComponent: Button { + objectName: "short" + implicitHeight: 25 + implicitWidth: 50 + } + } + ] + } + } + + function getChild(toolbar, objectName) { + let c = toolbar.children[0].children + for (let i in c) { + if (c[i].objectName === objectName) { + return c[i] + } + } + return -1 + } + + function test_height() { + var toolbar = createTemporaryObject(heightMode, testCase, {width: testCase.width}) + + verify(toolbar) + verify(waitForRendering(toolbar)) + + while (toolbar.visibleWidth == 0) { + // Same as above + wait(50) + } + + compare(toolbar.implicitHeight, 50) + compare(toolbar.height, 50) + + toolbar.customHeight = 100 + + // Changing the delegate height will trigger the internal layout to + // relayout, which is done in polish. This is not signaled to the + // parent toolbar, so we need to wait on the contentItem here. + verify(isPolishScheduled(toolbar.contentItem)) + verify(waitForItemPolished(toolbar.contentItem)) + + // Implicit height changes should propagate to the layout's height as + // long as that doesn't have an explicit height set. + compare(toolbar.implicitHeight, 100) + compare(toolbar.height, 100) + + // This should be the default, but make sure to set it regardless + toolbar.heightMode = Kirigami.ToolBarLayout.ConstrainIfLarger + toolbar.height = 50 + + // Find the actual child items so we can check their properties + let t = getChild(toolbar, "tall"); + let s = getChild(toolbar, "short"); + + // waitForItemPolished doesn't wait long enough and waitForRendering + // waits too long, so just wait an arbitrary amount of time... + wait(50) + + // ConstrainIfLarger should reduce the height of the first, but not touch the second + compare(t.height, 50) + compare(t.y, 0) + compare(s.height, 25) + compare(s.y, 13) // Should be centered and rounded + + // AlwaysCenter should not touch any item's height, only make sure they are centered + toolbar.heightMode = Kirigami.ToolBarLayout.AlwaysCenter + + wait(50) + + compare(t.height, 100) + compare(t.y, -25) + compare(s.height, 25) + compare(s.y, 13) + + // AlwaysFill should make sure each item has the same height as the toolbar + toolbar.heightMode = Kirigami.ToolBarLayout.AlwaysFill + + wait(50) + + compare(t.height, 50) + compare(t.y, 0) + compare(s.height, 50) + compare(s.y, 0) + + // Unconstraining the toolbar should reset its height to the maximum + // implicit height and set any children to that same value as heightMode + // is still AlwaysFill. + toolbar.height = undefined + + wait(50) + + compare(toolbar.height, 100) + compare(t.height, 100) + compare(s.height, 100) + } + + Component { + id: toolbarComponent + Kirigami.ToolBarLayout { + fullDelegate: Item {} + iconDelegate: Item {} + separatorDelegate: Item {} + moreButton: Item {} + } + } + + Component { + id: actionComponent + Kirigami.Action {} + } + + function test_dynamicActions() { + const toolbar = createTemporaryObject(toolbarComponent, this); + verify(toolbar); + + const actionA = createTemporaryObject(actionComponent, this, { text: "Action A" }); + verify(actionA) + toolbar.actions.push(actionA); + waitForPolish(toolbar); + actionA.destroy(); + wait(1500); // Let it destroy, and let toolBarLayout's throttle timer expire + + const actionB = createTemporaryObject(actionComponent, this, { text: "Action B" }); + verify(actionB) + + // shoud not crash + toolbar.actions.push(actionB); + waitForPolish(toolbar); + } + + function test_duplicateDynamicAction() { + const toolbar = createTemporaryObject(toolbarComponent, this); + verify(toolbar); + + const actionA = createTemporaryObject(actionComponent, this, { text: "Action A" }); + verify(actionA) + toolbar.actions.push(actionA); + toolbar.actions.push(actionA); + waitForPolish(toolbar); + actionA.destroy(); + wait(1500); // Let it destroy, and let toolBarLayout's throttle timer expire + + const actionB = createTemporaryObject(actionComponent, this, { text: "Action B" }); + verify(actionB) + + // shoud not crash + toolbar.actions.push(actionB); + waitForPolish(toolbar); + } +} diff --git a/autotests/tst_card.qml b/autotests/tst_card.qml new file mode 100644 index 0000000..9e37cde --- /dev/null +++ b/autotests/tst_card.qml @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2024 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import QtTest + +TestCase { + name: "Cards" + visible: true + when: windowShown + + width: 500 + height: 500 + + Component { + id: cardComponent + Kirigami.Card {} + } + + Component { + id: itemComponent + Item {} + } + + Component { + id: cardWithActionsComponent + Kirigami.Card { + actions: [ + QQC2.Action { + text: "QQC2" + }, + Kirigami.Action { + text: "Kirigami" + } + ] + } + } + + function test_init() { + const card = createTemporaryObject(cardComponent, this); + verify(card); + } + + function test_customFooter() { + const card = createTemporaryObject(cardComponent, this); + verify(card); + + const item = createTemporaryObject(itemComponent, this); + verify(item); + + const defaultFooter = card.footer; + verify(defaultFooter.visible); + verify(defaultFooter.parent !== null); + + card.footer = item; + verify(item.visible); + verify(!defaultFooter.visible); + } + + function test_cardWithActions() { + const card = createTemporaryObject(cardWithActionsComponent, this); + verify(card); + } +} diff --git a/autotests/tst_colorutils.qml b/autotests/tst_colorutils.qml new file mode 100644 index 0000000..d760eb9 --- /dev/null +++ b/autotests/tst_colorutils.qml @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2023 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtTest +import org.kde.kirigami as Kirigami + +TestCase { + id: testCase + name: "ColorUtilsTest" + + function test_linear_interpolation_data() { + return [ + {tag: "black to white", from: Qt.rgba(0.0, 0.0, 0.0), to: Qt.rgba(1.0, 1.0, 1.0), amount: 0.5, expected: Qt.rgba(0.5, 0.5, 0.5)}, + {tag: "white to black", from: Qt.rgba(1.0, 1.0, 1.0), to: Qt.rgba(0.0, 0.0, 0.0), amount: 0.5, expected: Qt.rgba(0.5, 0.5, 0.5)}, + {tag: "red to white", from: Qt.rgba(1.0, 0.0, 0.0), to: Qt.rgba(1.0, 1.0, 1.0), amount: 0.5, expected: Qt.rgba(1.0, 0.5, 0.5)}, + {tag: "green to white", from: Qt.rgba(0.0, 0.0, 1.0), to: Qt.rgba(1.0, 1.0, 1.0), amount: 0.5, expected: Qt.rgba(0.5, 0.5, 1.0)}, + {tag: "transparent to black", from: Qt.rgba(0.0, 0.0, 0.0, 0.0), to: Qt.rgba(0.0, 0.0, 0.0), amount: 0.5, expected: Qt.rgba(0.0, 0.0, 0.0, 0.5)}, + {tag: "transparent to white", from: Qt.rgba(0.0, 0.0, 0.0, 0.0), to: Qt.rgba(1.0, 1.0, 1.0), amount: 0.5, expected: Qt.rgba(0.5, 0.5, 0.5, 0.5)}, + // This would produce a pink color when not accounting for undefined hue when using white. + {tag: "broken separator", from: Qt.color("#af384c"), to: Qt.color("white"), amount: 0.15, expected: Qt.color("#bb4f61")} + ] + } + + function test_linear_interpolation(data) { + let result = Kirigami.ColorUtils.linearInterpolation(data.from, data.to, data.amount) + let expected = data.expected + + // According to the documentation, fuzzyCompare() should be able to handle + // color values but it seems to just do string comparison. So do a manual + // component-wise comparison instead. + fuzzyCompare(result.r, expected.r, 0.001, "Colors are not the same, Actual: " + result + " Expected: " + expected + ", component is red") + fuzzyCompare(result.g, expected.g, 0.001, "Colors are not the same, Actual: " + result + " Expected: " + expected + ", component is green") + fuzzyCompare(result.b, expected.b, 0.001, "Colors are not the same, Actual: " + result + " Expected: " + expected + ", component is blue") + fuzzyCompare(result.a, expected.a, 0.001, "Colors are not the same, Actual: " + result + " Expected: " + expected + ", component is alpha") + } +} + diff --git a/autotests/tst_columnview.qml b/autotests/tst_columnview.qml new file mode 100644 index 0000000..795339e --- /dev/null +++ b/autotests/tst_columnview.qml @@ -0,0 +1,363 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import QtTest + +TestCase { + name: "ColumnView" + visible: true + when: windowShown + + width: 500 + height: 500 + + Component { + id: columnViewComponent + Kirigami.ColumnView {} + } + + Component { + id: emptyItemPageComponent + Item {} + } + + function createViewWith3Items() { + const view = createTemporaryObject(columnViewComponent, this); + verify(view); + + const zero = createTemporaryObject(emptyItemPageComponent, this, { objectName: "zero" }); + view.addItem(zero); + + const one = createTemporaryObject(emptyItemPageComponent, this, { objectName: "one" }); + view.addItem(one); + + const two = createTemporaryObject(emptyItemPageComponent, this, { objectName: "two" }); + view.addItem(two); + + compare(view.count, 3); + + return ({ + view, + zero, + one, + two, + }); + } + + function test_clear() { + const { view } = createViewWith3Items(); + view.clear(); + compare(view.count, 0); + } + + function test_contains() { + const { view, zero, one, two } = createViewWith3Items(); + + verify(view.containsItem(zero)); + verify(view.containsItem(one)); + verify(view.containsItem(two)); + + view.removeItem(zero); + verify(!view.containsItem(zero)); + + view.addItem(zero); + verify(view.containsItem(zero)); + + verify(!view.containsItem(null)); + } + + function test_remove_by_index_leading() { + const { view, zero: target } = createViewWith3Items(); + const item = view.removeItem(0); + compare(item, target); + compare(view.count, 2); + } + + function test_remove_by_index_trailing() { + const { view, two: target } = createViewWith3Items(); + compare(view.count - 1, 2); + const item = view.removeItem(2); + compare(item, target); + compare(view.count, 2); + } + + function test_remove_by_index_middle() { + const { view, one: target } = createViewWith3Items(); + const item = view.removeItem(1); + compare(item, target); + compare(view.count, 2); + } + + function test_remove_by_index_last() { + const { view, zero, one, two } = createViewWith3Items(); + let item; + + item = view.removeItem(0); + compare(item, zero); + compare(view.count, 2); + + item = view.removeItem(1); + compare(item, two); + compare(view.count, 1); + + item = view.removeItem(0); + compare(item, one); + compare(view.count, 0); + } + + function test_remove_by_index_negative() { + const { view } = createViewWith3Items(); + const item = view.removeItem(-1); + compare(item, null); + compare(view.count, 3); + } + + function test_remove_by_index_out_of_bounds() { + const { view } = createViewWith3Items(); + const item = view.removeItem(view.count); + compare(item, null); + compare(view.count, 3); + } + + function test_remove_by_item_leading() { + const { view, zero: target } = createViewWith3Items(); + const item = view.removeItem(target); + compare(item, target); + compare(view.count, 2); + } + + function test_remove_by_item_trailing() { + const { view, two: target } = createViewWith3Items(); + const item = view.removeItem(target); + compare(item, target); + compare(view.count, 2); + } + + function test_remove_by_item_middle() { + const { view, one: target } = createViewWith3Items(); + const item = view.removeItem(target); + compare(item, target); + compare(view.count, 2); + } + + function test_remove_by_item_last() { + const { view, zero, one, two } = createViewWith3Items(); + let item; + + item = view.removeItem(zero); + compare(item, zero); + compare(view.count, 2); + + item = view.removeItem(one); + compare(item, one); + compare(view.count, 1); + + item = view.removeItem(two); + compare(item, two); + compare(view.count, 0); + } + + function test_remove_by_item_null() { + const { view } = createViewWith3Items(); + const item = view.removeItem(null); + compare(item, null); + compare(view.count, 3); + } + + function test_remove_by_item_from_empty() { + const view = createTemporaryObject(columnViewComponent, this); + verify(view); + let item; + + item = view.removeItem(null); + compare(item, null); + + item = view.removeItem(this); + compare(item, null); + } + + function test_pop_item_arg() { + const { view, zero, one, two } = createViewWith3Items(); + + compare(view.pop(one), two); + compare(view.count, 2); + compare(view.pop(zero), one); + compare(view.count, 1); + } + + function test_pop_index_arg() { + const { view, zero, one, two } = createViewWith3Items(); + + compare(view.pop(1), two); + compare(view.count, 2); + compare(view.pop(-1), zero); + compare(view.count, 0); + } + + function test_pop_no_args() { + const { view, zero, one, two } = createViewWith3Items(); + + compare(view.pop(), two); + compare(view.pop(), one); + compare(view.pop(), zero); + compare(view.pop(), null); + } + + function test_move() { + const { view, zero, one, two } = createViewWith3Items(); + + compare(view.contentChildren.length, 3); + compare(view.contentChildren[0], zero); + compare(view.contentChildren[2], two); + + view.moveItem(0, 2); + + compare(view.contentChildren[0], one); + compare(view.contentChildren[1], two); + compare(view.contentChildren[2], zero); + + // TODO: test currentIndex adjustments + } + + function test_insert_null() { + const { view } = createViewWith3Items(); + view.insertItem(0, null); + compare(view.count, 3); + } + + function test_insert_duplicate() { + const { view, one: target } = createViewWith3Items(); + view.insertItem(0, target); + compare(view.count, 3); + } + + function test_insert_leading() { + const { view, zero, one, two } = createViewWith3Items(); + const item = createTemporaryObject(emptyItemPageComponent, this, { objectName: "item" }); + view.insertItem(0, item); + compare(view.count, 4) + compare(view.contentChildren, [item, zero, one, two]); + } + + function test_insert_trailing() { + const { view, zero, one, two } = createViewWith3Items(); + const item = createTemporaryObject(emptyItemPageComponent, this, { objectName: "item" }); + view.insertItem(view.count, item); + compare(view.count, 4) + compare(view.contentChildren, [zero, one, two, item]); + } + + function test_insert_middle() { + const { view, zero, one, two } = createViewWith3Items(); + const item = createTemporaryObject(emptyItemPageComponent, this, { objectName: "item" }); + view.insertItem(2, item); + compare(view.count, 4) + compare(view.contentChildren, [zero, one, item, two]); + } + + function test_replace_middle() { + const { view, zero, one, two } = createViewWith3Items(); + const item = createTemporaryObject(emptyItemPageComponent, this, { objectName: "item" }); + view.replaceItem(1, item); + compare(view.count, 3) + compare(view.contentChildren, [zero, item, two]); + } + + function test_attached_index() { + const { view, zero, one, two } = createViewWith3Items(); + + compare(zero.Kirigami.ColumnView.index, 0); + compare(one.Kirigami.ColumnView.index, 1); + compare(two.Kirigami.ColumnView.index, 2); + } + + component Filler : Rectangle { + z: 1 + opacity: 0.2 + color: "#1EA8F7" + border.color: "black" + border.width: 1 + radius: 11 + implicitWidth: 100 + height: parent.height + } + + component Page : Rectangle { + id: page + + z: 0 + opacity: 0.2 + color: "#CF271C" + border.color: "black" + border.width: 1 + radius: 11 + height: parent.height + + MouseArea { + anchors.fill: parent + onClicked: mouse => { + page.Kirigami.ColumnView.view.currentIndex = page.Kirigami.ColumnView.index; + } + } + } + + Component { + id: clippingColumnViewComponent + Row { + readonly property Kirigami.ColumnView columnView: columnView + + width: 300 + height: 100 + + Filler {} + Kirigami.ColumnView { + id: columnView + + height: 100 + width: 100 + + columnWidth: 80 + scrollDuration: 0 + + Page {} + Page {} + Page {} + } + Filler {} + } + } + + function test_clicks_outside() { + const layout = createTemporaryObject(clippingColumnViewComponent, this); + const { columnView } = layout; + compare(columnView.count, 3); + waitForPolish(columnView); + + mouseClick(layout); + compare(columnView.currentIndex, 0); + + mouseClick(layout, 250); // center of trailing filler + compare(columnView.currentIndex, 0); + + mouseClick(layout, 50); // center of leading filler + compare(columnView.currentIndex, 0); + + mouseClick(layout, 190); // where the next page begins + compare(columnView.currentIndex, 1); + + mouseClick(layout, 190); // where the next page begins + compare(columnView.currentIndex, 2); + + mouseClick(layout, 50); + compare(columnView.currentIndex, 2); // does not move + columnView.clip = false; + mouseClick(layout, 50); + compare(columnView.currentIndex, 1); // moves + } +} diff --git a/autotests/tst_delegates.qml b/autotests/tst_delegates.qml new file mode 100644 index 0000000..85243e0 --- /dev/null +++ b/autotests/tst_delegates.qml @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami.delegates as KD +import QtTest + +TestCase { + name: "DelegatesTest" + visible: true + when: windowShown + + width: 500 + height: 500 + + Component { + id: subtitleDelegate + KD.SubtitleDelegate {} + } + + Component { + id: checkSubtitleDelegate + KD.CheckSubtitleDelegate {} + } + + Component { + id: radioSubtitleDelegate + KD.RadioSubtitleDelegate {} + } + + Component { + id: switchSubtitleDelegate + KD.SwitchSubtitleDelegate {} + } + + function test_create() { + failOnWarning(/error/i); + { + const delegate = createTemporaryObject(subtitleDelegate, this); + verify(delegate); + } + { + const delegate = createTemporaryObject(checkSubtitleDelegate, this); + verify(delegate); + } + { + const delegate = createTemporaryObject(radioSubtitleDelegate, this); + verify(delegate); + } + { + const delegate = createTemporaryObject(switchSubtitleDelegate, this); + verify(delegate); + } + } +} diff --git a/autotests/tst_dialogs.qml b/autotests/tst_dialogs.qml new file mode 100644 index 0000000..962e4f7 --- /dev/null +++ b/autotests/tst_dialogs.qml @@ -0,0 +1,134 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami +import QtTest + +TestCase { + name: "KirigamiDialogsTest" + visible: true + when: windowShown + + width: 500 + height: 500 + + Component { + id: dialogComponent + Kirigami.Dialog { + id: dialog + + readonly property SignalSpy acceptedSpy: SignalSpy { + target: dialog + signalName: "accepted" + } + readonly property SignalSpy rejectedSpy: SignalSpy { + target: dialog + signalName: "rejected" + } + readonly property Kirigami.Action kActionA: Kirigami.Action { + text: "Kirigami Action A" + property int count: 0 + onTriggered: count += 1; + } + readonly property Kirigami.Action kActionB: Kirigami.Action { + text: "Kirigami Action B" + visible: false + property int count: 0 + onTriggered: count += 1; + } + readonly property Kirigami.Action kActionC: Kirigami.Action { + text: "Kirigami Action C" + property int count: 0 + onTriggered: count += 1; + } + + title: "Dialog" + preferredWidth: 400 + customFooterActions: [kActionA, kActionB, kActionC] + } + } + + function test_footer_buttons() { + const dialog = createTemporaryObject(dialogComponent, this, { + standardButtons: T.Dialog.Ok | T.Dialog.Cancel, + }); + verify(dialog); + const { kActionA, kActionB, kActionC, acceptedSpy, rejectedSpy } = dialog; + verify(acceptedSpy.valid); + verify(rejectedSpy.valid); + + dialog.open(); + tryCompare(dialog, "opened", true, Kirigami.Units.longDuration * 2); + + const buttonOk = dialog.standardButton(T.Dialog.Ok); + verify(buttonOk); + mouseClick(buttonOk); + compare(acceptedSpy.count, 1); + + dialog.open(); + tryCompare(dialog, "opened", true, Kirigami.Units.longDuration * 2); + + const buttonCancel = dialog.standardButton(T.Dialog.Cancel); + verify(buttonCancel); + mouseClick(buttonCancel); + compare(rejectedSpy.count, 1); + + dialog.open(); + tryCompare(dialog, "opened", true, Kirigami.Units.longDuration * 2); + + const buttonA = dialog.customFooterButton(kActionA); + verify(buttonA); + mouseClick(buttonA); + compare(kActionA.count, 1); + + const buttonB = dialog.customFooterButton(kActionB); + verify(!buttonB); + + const buttonC = dialog.customFooterButton(kActionC); + verify(buttonC); + mouseClick(buttonC); + compare(kActionC.count, 1); + } + + Component { + id: nullActionDialogComponent + Kirigami.Dialog { + id: dialog + + readonly property SignalSpy acceptedSpy: SignalSpy { + target: dialog + signalName: "accepted" + } + readonly property SignalSpy rejectedSpy: SignalSpy { + target: dialog + signalName: "rejected" + } + + title: "Dialog" + preferredWidth: 400 + visible: true + } + } + + function test_null_footer_action() { + const dialog = createTemporaryObject(nullActionDialogComponent, this); + verify(dialog); + + compare(dialog.customFooterActions.length, 0); + + let button; + + button = dialog.customFooterButton(null); + verify(!button); + + dialog.customFooterActions = [null]; + + button = dialog.customFooterButton(null); + verify(!button); + } +} diff --git a/autotests/tst_formlayout.qml b/autotests/tst_formlayout.qml new file mode 100644 index 0000000..410a068 --- /dev/null +++ b/autotests/tst_formlayout.qml @@ -0,0 +1,335 @@ +/* + * SPDX-FileCopyrightText: 2022 Connor Carney + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Window +import QtQuick.Layouts +import org.kde.kirigami as Kirigami +import QtTest + +TestCase { + id: testCase + name: "FormLayout" + + width: 400 + height: 400 + visible: true + + when: windowShown + + Component { + id: fractionalSizeRoundingComponent + Window { + property var item: fractionalSizeItem + width: 600 + height: 400 + Kirigami.FormLayout { + anchors.fill: parent + Item { + id: fractionalSizeItem + implicitWidth: 160.375 + implicitHeight: 17.001 + Layout.fillWidth: true + } + } + } + } + + function test_fractional_width_rounding() { + let window = createTemporaryObject(fractionalSizeRoundingComponent); + let item = window.item; + window.show(); + + verify(item.width >= item.implicitWidth, "implicit width should not be rounded down"); + fuzzyCompare(item.width, item.implicitWidth, 1); + + window.close(); + } + + function test_fractional_height_rounding() { + let window = createTemporaryObject(fractionalSizeRoundingComponent); + let item = window.item; + window.show(); + + verify(item.height >= item.implicitHeight, "implicit height should not be rounded down"); + fuzzyCompare(item.height, item.implicitHeight, 1); + + window.close(); + } + + + Component { + id: dynamicBuddyFormComponent + + Kirigami.FormLayout { + id: form + + readonly property string labelText: "You found me!" + readonly property alias buddyColumn: buddyColumn + readonly property alias target1: target1 + readonly property alias target2: target2 + readonly property alias target3: target3 + + wideMode: true + + ColumnLayout { + id: buddyColumn + + spacing: 0 + + Kirigami.FormData.label: form.labelText + + Rectangle { + id: target1 + implicitWidth: 100 + implicitHeight: 100 + color: "red" + } + Rectangle { + id: target2 + implicitWidth: 100 + implicitHeight: 100 + color: "green" + Rectangle { + id: target3 + anchors.left: parent.left + anchors.bottom: parent.bottom + implicitWidth: 100 + implicitHeight: 100 + color: "blue" + } + } + } + } + } + + function findChildLabel(parent: Item, text: string): Text { + for (let i = 0; i < parent.children.length; i++) { + const child = parent.children[i]; + if ((child instanceof Text) && (child.text === text)) { + return child; + } else { + const label = findChildLabel(child, text); + if (label !== null) { + return label; + } + } + } + return null; + } + + function getYOffsetOfLabel(form: Kirigami.FormLayout, label: Item): real { + return label.mapToItem(form, 0, 0).y; + } + + function test_dynamicBuddyFor() { + const form = createTemporaryObject(dynamicBuddyFormComponent, this); + compare(form.buddyColumn.Kirigami.FormData.buddyFor, form.buddyColumn); + + const label = findChildLabel(form, form.labelText); + verify(label); + + form.buddyColumn.Kirigami.FormData.buddyFor = form.target1; + compare(form.buddyColumn.Kirigami.FormData.buddyFor, form.target1); + wait(100); // Unfortunately, this is needed due to async timer-based updates of FormLayout + const offset1 = getYOffsetOfLabel(form, label); + + form.buddyColumn.Kirigami.FormData.buddyFor = form.target2; + compare(form.buddyColumn.Kirigami.FormData.buddyFor, form.target2); + wait(100); + const offset2 = getYOffsetOfLabel(form, label); + + verify(offset1 < offset2); + } + + function test_nestedBuddyNotSupported() { + const form = createTemporaryObject(dynamicBuddyFormComponent, this); + compare(form.buddyColumn.Kirigami.FormData.buddyFor, form.buddyColumn); + + ignoreWarning(/FormData.buddyFor must be a direct child of the attachee.*/); + form.buddyColumn.Kirigami.FormData.buddyFor = form.target3; + // shouldn't change + compare(form.buddyColumn.Kirigami.FormData.buddyFor, form.buddyColumn); + } + + SignalSpy { + id: buddyChangeSpy + signalName: "buddyForChanged" + } + + Component { + id: buddyRepeaterFormComponent + + Kirigami.FormLayout { + id: form + + readonly property string labelText: "You found me!" + readonly property alias buddyColumn: buddyColumn + property alias repeaterModel: repeater.model + property Item buddyCreatedByRepeater + + wideMode: true + + ColumnLayout { + id: buddyColumn + + spacing: 0 + + Kirigami.FormData.label: form.labelText + + Repeater { + id: repeater + + model: 0 + + Rectangle { + implicitWidth: 100 + implicitHeight: 100 + color: "red" + } + + onItemAdded: (index, item) => { + form.buddyCreatedByRepeater = item; + buddyColumn.Kirigami.FormData.buddyFor = item; + } + } + } + } + } + + function test_buddyCreatedAndDestroyedByRepeater() { + // The point is to test automatic destruction as done by a Repeater + + const form = createTemporaryObject(buddyRepeaterFormComponent, this); + compare(form.buddyColumn.Kirigami.FormData.buddyFor, form.buddyColumn); + + buddyChangeSpy.target = form.buddyColumn.Kirigami.FormData; + buddyChangeSpy.clear(); + verify(buddyChangeSpy.valid); + + form.repeaterModel = 1; + + verify(form.buddyCreatedByRepeater); + compare(form.buddyColumn.Kirigami.FormData.buddyFor, form.buddyCreatedByRepeater); + compare(buddyChangeSpy.count, 1); + + form.repeaterModel = 0; + wait(100); // Give Repeater some time to react to model changes. + + verify(!form.buddyCreatedByRepeater); + compare(form.buddyColumn.Kirigami.FormData.buddyFor, form.buddyColumn); + compare(buddyChangeSpy.count, 2); + } + + Component { + id: buddyComponentFormComponent + + Kirigami.FormLayout { + id: form + + readonly property string labelText: "You found me!" + readonly property alias buddyColumn: buddyColumn + + function addBuddyFromComponent(component: Component): Item { + const buddy = component.createObject(buddyColumn); + buddyColumn.Kirigami.FormData.buddyFor = buddy; + return buddy; + } + + wideMode: true + + ColumnLayout { + id: buddyColumn + + spacing: 0 + + Kirigami.FormData.label: form.labelText + } + } + } + + Component { + id: buddyComponent + + Rectangle { + implicitWidth: 100 + implicitHeight: 100 + color: "red" + } + } + + function test_buddyCreatedAndDestroyedByComponent() { + // The point is to test manual destruction as done by calling destroy() + + const form = createTemporaryObject(buddyComponentFormComponent, this); + compare(form.buddyColumn.Kirigami.FormData.buddyFor, form.buddyColumn); + + buddyChangeSpy.target = form.buddyColumn.Kirigami.FormData; + buddyChangeSpy.clear(); + verify(buddyChangeSpy.valid); + + const buddy = form.addBuddyFromComponent(buddyComponent); + verify(buddy); + compare(form.buddyColumn.Kirigami.FormData.buddyFor, buddy); + compare(buddyChangeSpy.count, 1); + + wait(100); + + buddy.destroy(); + + wait(100); + + // should revert back to parent + compare(form.buddyColumn.Kirigami.FormData.buddyFor, form.buddyColumn); + compare(buddyChangeSpy.count, 2); + } + + Component { + id: enabledTestFormLayoutComponent + + Kirigami.FormLayout { + property alias normalItem: normalItem + property alias buddyRow: buddyRow + property alias buddyItem: buddyItem + + // Normal case, direct item + Item { + id: normalItem + Kirigami.FormData.label: "Normal case" + } + + RowLayout { + id: buddyRow + Kirigami.FormData.label: "Buddy case" + Kirigami.FormData.buddyFor: buddyItem + + Item { + id: buddyItem + } + } + } + } + + function test_enabledPropagation() { + const form = createTemporaryObject(enabledTestFormLayoutComponent, this); + + const normalLabel = findChildLabel(form, "Normal case"); + verify(normalLabel.enabled); + + form.normalItem.enabled = false; + verify(!normalLabel.enabled); + + const buddyLabel = findChildLabel(form, "Buddy case"); + verify(buddyLabel.enabled); + + form.buddyItem.enabled = false; + verify(!buddyLabel.enabled); + + // Unset buddy, should be enabled again. + form.buddyRow.Kirigami.FormData.buddyFor = null; + verify(buddyLabel.enabled); + } +} diff --git a/autotests/tst_globaldrawer.qml b/autotests/tst_globaldrawer.qml new file mode 100644 index 0000000..82e088c --- /dev/null +++ b/autotests/tst_globaldrawer.qml @@ -0,0 +1,152 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami +import QtTest + +// Inline components are needed because ApplicationItem and +// ApplicationWindow types expect themselves to be top-level components. +TestCase { + name: "GlobalDrawerHeader" + visible: true + when: windowShown + + width: 500 + height: 500 + + component AppItemComponent : Kirigami.ApplicationItem { + id: app + + property alias headerItem: headerItem + property alias topItem: topItem + + width: 500 + height: 500 + visible: true + + globalDrawer: Kirigami.GlobalDrawer { + drawerOpen: true + + header: Rectangle { + id: headerItem + implicitHeight: 50 + implicitWidth: 50 + color: "red" + radius: 20 // to see its bounds + } + + // Create some item which we can use to measure actual header height + Rectangle { + id: topItem + Layout.fillWidth: true + Layout.fillHeight: true + color: "green" + radius: 20 // to see its bounds + } + } + } + + Component { + id: appItemComponent + AppItemComponent {} + } + + function test_headerItemVisibility() { + if (Qt.platform.os === "unix") { + skip("On FreeBSD Qt 6.5 fails deep inside generated MOC code for `drawerOpen: true` binding"); + } + const app = createTemporaryObject(appItemComponent, this); + verify(app); + const { headerItem, topItem } = app; + + compare(app.globalDrawer.parent, app.T.Overlay.overlay); + + waitForRendering(app.globalDrawer.contentItem); + + const overlay = T.Overlay.overlay; + verify(headerItem.height !== 0); + + // Due to margins, position won't be exactly zero... + const position = topItem.mapToItem(overlay, 0, 0); + verify(position.y > 0); + const oldY = position.y; + + // ...but with visible header it would be greater than with invisible. + headerItem.visible = false; + tryVerify(() => { + const position = topItem.mapToItem(overlay, 0, 0); + return position.y < oldY; + }); + + // And now return it back to where we started. + headerItem.visible = true; + tryVerify(() => { + const position = topItem.mapToItem(overlay, 0, 0); + return position.y === oldY; + }); + } + + component AppItemLoaderComponent : Kirigami.ApplicationItem { + globalDrawer: globalDrawerLoader.item + contextDrawer: contextDrawerLoader.item + + Loader { + id: globalDrawerLoader + active: true + sourceComponent: Kirigami.GlobalDrawer {} + } + Loader { + id: contextDrawerLoader + active: true + sourceComponent: Kirigami.ContextDrawer {} + } + } + + Component { + id: appItemLoaderComponent + AppItemLoaderComponent {} + } + + component AppWindowLoaderComponent : Kirigami.ApplicationWindow { + globalDrawer: globalDrawerLoader.item + contextDrawer: contextDrawerLoader.item + + Loader { + id: globalDrawerLoader + active: true + sourceComponent: Kirigami.GlobalDrawer {} + } + Loader { + id: contextDrawerLoader + active: true + sourceComponent: Kirigami.ContextDrawer {} + } + } + + Component { + id: appWindowLoaderComponent + AppWindowLoaderComponent {} + } + + function test_reparentingFromLoader_data() { + return [ + { tag: "item", component: appItemLoaderComponent }, + { tag: "window", component: appWindowLoaderComponent }, + ]; + } + + function test_reparentingFromLoader({ component }) { + const app = createTemporaryObject(component, this); + verify(app); + + compare(app.globalDrawer.parent, app.T.Overlay.overlay); + compare(app.contextDrawer.parent, app.T.Overlay.overlay); + } +} diff --git a/autotests/tst_headerfooterlayout.qml b/autotests/tst_headerfooterlayout.qml new file mode 100644 index 0000000..a292d29 --- /dev/null +++ b/autotests/tst_headerfooterlayout.qml @@ -0,0 +1,278 @@ +/* + * SPDX-FileCopyrightText: 2023 Marco Martin + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import QtTest +import org.kde.kirigami as Kirigami + +TestCase { + id: testCase + + name: "HeaderFooterLayoutTests" + when: windowShown + + width: 300 + height: 300 + visible: true + + Component { + id: implicitSizeComponent + Kirigami.HeaderFooterLayout { + header: Rectangle { + color: "red" + implicitHeight: 30 + implicitWidth: 100 + } + contentItem: Rectangle { + color: "green" + implicitHeight: 40 + implicitWidth: 110 + } + footer: Rectangle { + color: "blue" + implicitHeight: 25 + implicitWidth: 120 + } + } + } + + Component { + id: nestedLayoutComponent + ColumnLayout { + Rectangle { + color: "red" + Layout.fillWidth: true + Layout.minimumHeight: 50 + } + Kirigami.HeaderFooterLayout { + Layout.fillWidth: true + Layout.fillHeight: true + header: Rectangle { + color: "red" + implicitHeight: 30 + implicitWidth: 100 + } + contentItem: Rectangle { + color: "green" + implicitHeight: 40 + implicitWidth: 110 + } + footer: Rectangle { + color: "blue" + implicitHeight: 25 + implicitWidth: 120 + } + } + } + } + + function test_implicit_sizes_standalone_layout() { + const layout = createTemporaryObject(implicitSizeComponent, this); + verify(layout); + + compare(layout.implicitHeight, 95); + compare(layout.implicitWidth, 120); + compare(layout.height, 95); + compare(layout.width, 120); + + compare(layout.header.width, 120); + compare(layout.header.height, 30); + + compare(layout.footer.width, 120); + compare(layout.footer.height, 25); + + compare(layout.contentItem.width, 120); + compare(layout.contentItem.height, 40); + + layout.height = 130; + verify(waitForItemPolished(layout)); + + // Header and footer don't change + compare(layout.header.width, 120); + compare(layout.header.height, 30); + + compare(layout.footer.width, 120); + compare(layout.footer.height, 25); + + // ContentItem stretchesvertically + compare(layout.contentItem.width, 120); + compare(layout.contentItem.height, 75); + + layout.width = 200; + verify(waitForItemPolished(layout)); + // Everything stretched only horizontally + compare(layout.header.width, 200); + compare(layout.header.height, 30); + + compare(layout.footer.width, 200); + compare(layout.footer.height, 25); + + compare(layout.contentItem.width, 200); + compare(layout.contentItem.height, 75); + + // change header implicit size + layout.header.implicitHeight = 40; + verify(waitForItemPolished(layout)); + compare(layout.implicitHeight, 105); + compare(layout.implicitWidth, 120); + compare(layout.height, 130); + compare(layout.width, 200); + + compare(layout.header.width, 200); + compare(layout.header.height, 40); + + compare(layout.footer.width, 200); + compare(layout.footer.height, 25); + + compare(layout.contentItem.width, 200); + compare(layout.contentItem.height, 65); + + // hide header + layout.header.visible = false; + verify(waitForItemPolished(layout)); + compare(layout.implicitHeight, 65); + compare(layout.implicitWidth, 120); + compare(layout.height, 130); + compare(layout.width, 200); + + compare(layout.header.width, 200); + compare(layout.header.height, 40); + + compare(layout.footer.width, 200); + compare(layout.footer.height, 25); + + compare(layout.contentItem.width, 200); + compare(layout.contentItem.height, 105); + + // force polish right now + verify(!isPolishScheduled(layout)); + layout.header.visible = true; + verify(isPolishScheduled(layout)); + compare(layout.implicitHeight, 65); + layout.forceLayout(); + compare(layout.implicitHeight, 105); + verify(waitForItemPolished(layout)); + compare(layout.implicitHeight, 105); + + // Change the spacing + layout.spacing = 5; + verify(waitForItemPolished(layout)); + compare(layout.contentItem.height, 55); + compare(layout.implicitHeight, 115); + } + + function test_implicit_sizes_nested_layout() { + const columnLayout = createTemporaryObject(nestedLayoutComponent, this); + verify(columnLayout); + const headerFooterLayout = columnLayout.children[1]; + verify(waitForRendering(columnLayout)); + verify(headerFooterLayout instanceof Kirigami.HeaderFooterLayout); + + compare(columnLayout.implicitHeight, 95 + 50 + columnLayout.spacing); + compare(columnLayout.implicitHeight, columnLayout.height); + + compare(headerFooterLayout.implicitHeight, 95); + compare(headerFooterLayout.implicitWidth, 120); + compare(headerFooterLayout.height, 95); + compare(headerFooterLayout.width, 120); + + compare(headerFooterLayout.header.width, 120); + compare(headerFooterLayout.header.height, 30); + + compare(headerFooterLayout.footer.width, 120); + compare(headerFooterLayout.footer.height, 25); + + compare(headerFooterLayout.contentItem.width, 120); + compare(headerFooterLayout.contentItem.height, 40); + + columnLayout.height = 200; + verify(waitForItemPolished(columnLayout)); + verify(waitForItemPolished(headerFooterLayout)); + + // headerFooterLayout should have stretched + compare(headerFooterLayout.implicitHeight, 95); + compare(headerFooterLayout.implicitWidth, 120); + compare(headerFooterLayout.height, 145); + compare(headerFooterLayout.width, 120); + + compare(headerFooterLayout.header.width, 120); + compare(headerFooterLayout.header.height, 30); + + compare(headerFooterLayout.footer.width, 120); + compare(headerFooterLayout.footer.height, 25); + + compare(headerFooterLayout.contentItem.width, 120); + compare(headerFooterLayout.contentItem.height, 90); + + // change header implicit size + headerFooterLayout.header.implicitHeight = 40; + verify(waitForItemPolished(columnLayout)); + verify(waitForItemPolished(headerFooterLayout)); + + compare(headerFooterLayout.implicitHeight, 105); + compare(headerFooterLayout.implicitWidth, 120); + compare(headerFooterLayout.height, 145); + compare(headerFooterLayout.width, 120); + + compare(headerFooterLayout.header.width, 120); + compare(headerFooterLayout.header.height, 40); + + compare(headerFooterLayout.footer.width, 120); + compare(headerFooterLayout.footer.height, 25); + + compare(headerFooterLayout.contentItem.width, 120); + compare(headerFooterLayout.contentItem.height, 80); + + // hide header + headerFooterLayout.header.visible = false; + verify(waitForItemPolished(columnLayout)); + verify(waitForItemPolished(headerFooterLayout)); + + compare(headerFooterLayout.implicitHeight, 65); + compare(headerFooterLayout.implicitWidth, 120); + compare(headerFooterLayout.height, 145); + compare(headerFooterLayout.width, 120); + + compare(headerFooterLayout.header.width, 120); + compare(headerFooterLayout.header.height, 40); + + compare(headerFooterLayout.footer.width, 120); + compare(headerFooterLayout.footer.height, 25); + + compare(headerFooterLayout.contentItem.width, 120); + compare(headerFooterLayout.contentItem.height, 120); + } + + function test_disconnect_old_items() { + const layout = createTemporaryObject(implicitSizeComponent, this); + verify(layout); + + verify(isPolishScheduled(layout)); + verify(waitForItemPolished(layout)); + + for (const partName of ["header", "contentItem", "footer"]) { + const part = layout[partName]; + + part.implicitHeight = 10; + + verify(isPolishScheduled(layout)); + verify(waitForItemPolished(layout)); + + layout[partName] = null; + + verify(isPolishScheduled(layout)); + verify(waitForItemPolished(layout)); + + part.implicitHeight = 20; + + verify(!isPolishScheduled(layout)); + } + + } +} diff --git a/autotests/tst_icon.qml b/autotests/tst_icon.qml new file mode 100644 index 0000000..dd7747f --- /dev/null +++ b/autotests/tst_icon.qml @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtTest +import org.kde.kirigami as Kirigami + +TestCase { + id: testCase + name: "IconTests" + + width: 400 + height: 400 + visible: true + + when: windowShown + + Component { id: emptyIcon; Kirigami.Icon { } } + Component { id: sourceOnlyIcon; Kirigami.Icon { source: "document-new" } } + Component { id: sizeOnlyIcon; Kirigami.Icon { width: 50; height: 50 } } + Component { id: sizeSourceIcon; Kirigami.Icon { width: 50; height: 50; source: "document-new" } } + Component { id: minimalSizeIcon; Kirigami.Icon { width: 1; height: 1; source: "document-new" } } + Component { + id: absolutePathIcon; + Kirigami.Icon { + id: icon + width: 50; + height: 50; + source: Qt.resolvedUrl("stop-icon.svg") + } + } + Kirigami.ImageColors { + id: imageColors + } + + function test_create_data() { + return [ + { tag: "Empty", component: emptyIcon }, + { tag: "Source Only", component: sourceOnlyIcon }, + { tag: "Size Only", component: sizeOnlyIcon }, + { tag: "Size & Source", component: sizeSourceIcon }, + { tag: "Minimal Size", component: minimalSizeIcon } + ] + } + + // Test creation of Icon objects. + // It should not crash when certain properties are not specified and also + // should still work when they are. + function test_create(data) { + var icon = createTemporaryObject(data.component, testCase) + verify(icon) + verify(waitForRendering(icon)) + } + + function test_absolutepath_recoloring() { + var icon = createTemporaryObject(absolutePathIcon, testCase) + verify(icon) + verify(waitForRendering(icon)) + + var image = icon.grabToImage(function(result) { + // Access pixel data of the captured image + imageColors.source = result.image + imageColors.update() + }) + tryCompare(imageColors, "dominant", "#2980b9") + } +} diff --git a/autotests/tst_inlinemessage.qml b/autotests/tst_inlinemessage.qml new file mode 100644 index 0000000..0c0b98c --- /dev/null +++ b/autotests/tst_inlinemessage.qml @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtTest +import org.kde.kirigami as Kirigami + +TestCase { + id: testCase + + name: "InlineMessageTests" + when: windowShown + + width: 300 + height: 300 + visible: true + + Component { + id: inlineMessageComponent + Kirigami.InlineMessage { + id: message + + readonly property SignalSpy linkHoveredSpy: SignalSpy { + target: message + signalName: "linkHovered" + } + readonly property SignalSpy linkActivatedSpy: SignalSpy { + target: message + signalName: "linkActivated" + } + + anchors { + top: parent.top + left: parent.left + right: parent.right + } + visible: true + } + } + + function hoverAll(item: Item, /*predicate*/ until) { + for (let x = 0; x < item.width; x += 10) { + for (let y = 0; y < item.height; y += 10) { + mouseMove(item, x, y); + if (until()) { + return Qt.point(x, y); + } + } + } + return null; + } + + function test_link() { + skip("finding links by sweeping with the mouse cursor all over the place seems very unreliable, especially on FreeBSD and Windows TODO: find a better way to find links") + const href = "some"; + const message = createTemporaryObject(inlineMessageComponent, this, { + text: `link`, + }); + verify(message); + verify(message.linkHoveredSpy.valid); + verify(message.linkActivatedSpy.valid); + + const point = hoverAll(message, () => message.hoveredLink === href); + verify(point !== null); + compare(message.linkHoveredSpy.count, 1); + const hoveredLink = message.linkHoveredSpy.signalArguments[0][0]; + compare(hoveredLink, href); + + mouseClick(message, point.x, point.y); + compare(message.linkActivatedSpy.count, 1); + const activatedLink = message.linkActivatedSpy.signalArguments[0][0]; + compare(activatedLink, href); + } + + function test_hoveredLink_is_readonly() { + const message = createTemporaryObject(inlineMessageComponent, this); + verify(message); + compare(message.hoveredLink, ""); + let failed = false; + try { + message.hoveredLink = "something"; + } catch (e) { + failed = true; + } + verify(failed); + } +} diff --git a/autotests/tst_inlineviewheader.qml b/autotests/tst_inlineviewheader.qml new file mode 100644 index 0000000..13b8408 --- /dev/null +++ b/autotests/tst_inlineviewheader.qml @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtTest +import org.kde.kirigami as Kirigami + +TestCase { + id: testCase + + name: "InlineViewHeaderTests" + when: windowShown + + width: 300 + height: 300 + visible: true + + Component { + id: emptyComponent + Kirigami.InlineViewHeader {} + } + + Component { + id: actionableComponent + Kirigami.InlineViewHeader { + readonly property Kirigami.Action kActionA: Kirigami.Action { + text: "Kirigami Action A" + } + readonly property Kirigami.Action kActionB: Kirigami.Action { + text: "Kirigami Action B" + visible: false + } + readonly property Kirigami.Action kActionC: Kirigami.Action { + text: "Kirigami Action C" + } + + actions: [kActionA, kActionB, kActionC] + } + } + + function test_sizing() { + const emptyHeader = createTemporaryObject(emptyComponent, this); + verify(emptyHeader); + const labelOnlyHeader = createTemporaryObject(emptyComponent, this, { text: "Name" }); + verify(labelOnlyHeader); + const multiLinelabelOnlyHeader = createTemporaryObject(emptyComponent, this, { + text: "First Line followed by\nSecond Line" + }); + verify(multiLinelabelOnlyHeader); + const actionsOnlyHeader = createTemporaryObject(actionableComponent, this); + verify(actionsOnlyHeader); + const fullHeader = createTemporaryObject(actionableComponent, this, { text: "Name" }); + verify(fullHeader); + // Let them polish and instantiate delegates + wait(1100); + + // check that implicit height is more than just vertical padding + verify(emptyHeader.implicitHeight > emptyHeader.topPadding + emptyHeader.bottomPadding); + + // implicit height stays the same regardless of text label content: + compare(emptyHeader.implicitHeight, labelOnlyHeader.implicitHeight); + + // label elides instead of wrapping: + compare(emptyHeader.implicitHeight, multiLinelabelOnlyHeader.implicitHeight); + + // label contributes to the width + verify(labelOnlyHeader.implicitWidth > labelOnlyHeader.leftPadding + labelOnlyHeader.rightPadding) + + compare(actionsOnlyHeader.implicitHeight, fullHeader.implicitHeight); + + verify(fullHeader.implicitWidth > actionsOnlyHeader.implicitWidth); + verify(actionsOnlyHeader.implicitWidth > emptyHeader.implicitWidth); + verify(labelOnlyHeader.implicitWidth > emptyHeader.implicitWidth); + } +} diff --git a/autotests/tst_keynavigation.qml b/autotests/tst_keynavigation.qml new file mode 100644 index 0000000..97c59d5 --- /dev/null +++ b/autotests/tst_keynavigation.qml @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Window +import org.kde.kirigami as Kirigami +import QtTest +import "../tests" + +TestCase { + id: testCase + width: 400 + height: 400 + name: "KeyboardNavigation" + + Component { + id: mainComponent + KeyboardTest { + id: window + + width: 480 + height: 360 + + readonly property SignalSpy spyLastKey: SignalSpy { + target: window.pageStack.currentItem + signalName: "lastKeyChanged" + } + } + } + + // The following methods are adaptation of QtTest internals + + function waitForWindowActive(window: Window) { + tryVerify(() => window.active); + } + + function ensureWindowShown(window: Window) { + window.requestActivate(); + waitForWindowActive(window); + wait(0); + } + + function test_press() { + const window = createTemporaryObject(mainComponent, this); + verify(window); + const spy = window.spyLastKey; + verify(spy.valid); + + ensureWindowShown(window); + + compare(window.pageStack.depth, 2); + compare(window.pageStack.currentIndex, 1); + + let keyCount = 0; + + keyClick("A"); + keyCount += 1; + compare(spy.count, keyCount); + compare(window.pageStack.currentItem.lastKey, "A"); + + keyClick(Qt.Key_Left, Qt.AltModifier); + keyCount += 1; + compare(spy.count, keyCount); + compare(window.pageStack.currentIndex, 0); + compare(window.pageStack.currentItem.lastKey, ""); + + keyClick("B") + keyCount += 1; + compare(spy.count, keyCount); + compare(window.pageStack.currentItem.lastKey, "B"); + } +} diff --git a/autotests/tst_listskeynavigation.qml b/autotests/tst_listskeynavigation.qml new file mode 100644 index 0000000..a82f31d --- /dev/null +++ b/autotests/tst_listskeynavigation.qml @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2016 Marco Martin + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Window +import org.kde.kirigami as Kirigami +import QtTest +import "../tests" + +TestCase { + id: testCase + width: 400 + height: 400 + name: "KeyboardListsNavigation" + + Component { + id: mainComponent + KeyboardListTest { + id: window + + width: 480 + height: 360 + } + } + + Component { + id: spyComponent + SignalSpy {} + } + + // The following methods are adaptation of QtTest internals + + function waitForWindowActive(window: Window) { + tryVerify(() => window.active); + } + + function ensureWindowShown(window: Window) { + window.requestActivate(); + waitForWindowActive(window); + wait(0); + } + + function test_press() { + const window = createTemporaryObject(mainComponent, this); + verify(window); + const spy = createTemporaryObject(spyComponent, this, { + target: window.pageStack.currentItem.flickable, + signalName: "currentIndexChanged", + }) + verify(spy.valid); + + ensureWindowShown(window); + + compare(window.pageStack.depth, 1); + compare(window.pageStack.currentIndex, 0); + + compare(window.pageStack.currentItem.flickable.currentIndex, 0); + keyClick(Qt.Key_Down); + compare(spy.count, 1); + compare(window.pageStack.currentItem.flickable.currentIndex, 1); + } +} diff --git a/autotests/tst_menudialog.qml b/autotests/tst_menudialog.qml new file mode 100644 index 0000000..2f9bfdc --- /dev/null +++ b/autotests/tst_menudialog.qml @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami +import QtTest + +TestCase { + id: root + + name: "MenuDialogTest" + visible: true + when: windowShown + + width: 300 + height: 300 + + Component { + id: menuDialogComponent + Kirigami.MenuDialog { + readonly property Kirigami.Action actionA: Kirigami.Action { + text: "Action A" + } + + preferredWidth: 200 + + actions: [actionA] + } + } + + Component { + id: spyComponent + SignalSpy {} + } + + function findChildIf(parent: Item, predicate /*(Item) -> bool*/): Item { + for (const child of parent.children) { + if (predicate(child)) { + return child; + } else { + const item = findChildIf(child, predicate); + if (item !== null) { + return item; + } + } + } + return null; + } + + function testClosed() { + const dialog = createTemporaryObject(this, menuDialogComponent); + verify(dialog); + + const { actionA } = dialog; + + const dialogClosedSpy = createTemporaryObject(this, spyComponent, { + target: dialog, + signalName: "closed", + }); + const actionSpy = createTemporaryObject(this, spyComponent, { + target: actionA, + signalName: "triggered", + }); + + dialog.open(); + tryVerify(() => dialog.opened); + + const delegate = findChildIf(dialog.contentItem, item => item.action === actionA) as QQC2.ItemDelegate; + verify(delegate); + + mouseClick(delegate); + compare(actionSpy.count, 1); + compare(dialogClosedSpy.count, 1); + tryVerify(() => !dialog.visible); + } +} diff --git a/autotests/tst_mnemonicdata.qml b/autotests/tst_mnemonicdata.qml new file mode 100644 index 0000000..0f36a99 --- /dev/null +++ b/autotests/tst_mnemonicdata.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Window +import org.kde.kirigami as Kirigami +import QtTest +import "../tests" + +TestCase { + id: testCase + + Kirigami.MnemonicData.enabled: true + Kirigami.MnemonicData.label: "设置(&S)" + + width: 400 + height: 400 + + SignalSpy { + id: sequenceChangedSpy + target: testCase.Kirigami.MnemonicData + signalName: "sequenceChanged" + } + + function test_press() { + compare(Kirigami.MnemonicData.richTextLabel, "设置") + } + + function test_disable_shortcut() { + compare(sequenceChangedSpy.count, 0); + testCase.Kirigami.MnemonicData.enabled = false; + compare(sequenceChangedSpy.count, 1); + testCase.Kirigami.MnemonicData.enabled = true; + compare(sequenceChangedSpy.count, 2); + } +} diff --git a/autotests/tst_navigationtabbar.qml b/autotests/tst_navigationtabbar.qml new file mode 100644 index 0000000..b10c508 --- /dev/null +++ b/autotests/tst_navigationtabbar.qml @@ -0,0 +1,129 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import QtTest +import org.kde.kirigami as Kirigami + +TestCase { + id: root + + name: "NavigationTabBarTest" + visible: true + when: windowShown + + width: 500 + height: 500 + + Component { + id: emptyComponent + Kirigami.NavigationTabBar {} + } + + Component { + id: tActionComponent + T.Action {} + } + + Component { + id: kActionComponent + Kirigami.Action {} + } + + function test_create() { + failOnWarning(/error|null/i); + { + const tabbar = createTemporaryObject(emptyComponent, this); + verify(tabbar); + } + { + const tAction = createTemporaryObject(tActionComponent, this); + const kAction = createTemporaryObject(kActionComponent, this); + const tabbar = createTemporaryObject(emptyComponent, this); + verify(tabbar); + tabbar.actions = [tAction, kAction]; + compare(tabbar.visibleActions, [tAction, kAction]); + + const kInvisibleAction = createTemporaryObject(kActionComponent, this, { + visible: false, + }); + verify(kInvisibleAction); + tabbar.actions.push(kInvisibleAction); + compare(tabbar.visibleActions, [tAction, kAction]); + + tabbar.actions.push(null); + compare(tabbar.visibleActions, [tAction, kAction]); + + const tAction2 = createTemporaryObject(tActionComponent, this); + tabbar.actions.push(tAction2); + compare(tabbar.visibleActions, [tAction, kAction, tAction2]); + } + } + + Component { + id: pageWithHeader + QQC2.Page { + header: Kirigami.NavigationTabBar {} + } + } + + Component { + id: pageWithFooter + QQC2.Page { + footer: Kirigami.NavigationTabBar {} + } + } + + function test_qqc2_page_position() { + { + const page = createTemporaryObject(pageWithHeader, this); + verify(page); + const bar = page.header; + verify(bar instanceof Kirigami.NavigationTabBar); + compare(bar.position, QQC2.ToolBar.Header); + } + { + const page = createTemporaryObject(pageWithFooter, this); + verify(page); + const bar = page.footer; + verify(bar instanceof Kirigami.NavigationTabBar); + compare(bar.position, QQC2.ToolBar.Footer); + } + } + + Component { + id: windowWithHeader + QQC2.ApplicationWindow { + header: Kirigami.NavigationTabBar {} + } + } + + Component { + id: windowWithFooter + QQC2.ApplicationWindow { + footer: Kirigami.NavigationTabBar {} + } + } + + function test_qqc2_window_position() { + { + const window = createTemporaryObject(windowWithHeader, this); + verify(window); + const bar = window.header; + verify(bar instanceof Kirigami.NavigationTabBar); + compare(bar.position, QQC2.ToolBar.Header); + } + { + const window = createTemporaryObject(windowWithFooter, this); + verify(window); + const bar = window.footer; + verify(bar instanceof Kirigami.NavigationTabBar); + compare(bar.position, QQC2.ToolBar.Footer); + } + } +} diff --git a/autotests/tst_overlaysheet.qml b/autotests/tst_overlaysheet.qml new file mode 100644 index 0000000..b025f61 --- /dev/null +++ b/autotests/tst_overlaysheet.qml @@ -0,0 +1,144 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import QtTest +import org.kde.kirigami as Kirigami + +TestCase { + id: root + + name: "OverlaySheetTest" + visible: true + when: windowShown + + width: 500 + height: 500 + + Component { + id: emptyComponent + Kirigami.OverlaySheet {} + } + + Component { + id: itemContentComponent + Kirigami.OverlaySheet { + Rectangle { + implicitWidth: 100 + implicitHeight: 200 + } + } + } + + Component { + id: viewContentWithImplicitSizeComponent + Kirigami.OverlaySheet { + ListView { + implicitWidth: 100 + implicitHeight: 200 + } + } + } + + Component { + id: viewContentWithPreferredSizeComponent + Kirigami.OverlaySheet { + ListView { + Layout.preferredWidth: 100 + Layout.preferredHeight: 200 + } + } + } + + Component { + id: itemContentWithMixedSizesComponent + Kirigami.OverlaySheet { + Rectangle { + implicitWidth: 100 + implicitHeight: 200 + + Layout.preferredWidth: 300 + Layout.preferredHeight: 400 + } + } + } + + Component { + id: viewContentWithMixedSizesComponent + Kirigami.OverlaySheet { + ListView { + implicitWidth: 100 + implicitHeight: 200 + + Layout.preferredWidth: 300 + Layout.preferredHeight: 400 + } + } + } + + function setupWarningsFilter() { + // no nullable property access errors are allowed + failOnWarning(/\bnull\b/); + } + + // Note about implicit size comparison: we don't know how much space + // header+footer+paddings would take, but the sum should be no less than + // the implicit or preferred size given. And the difference between + // implicit and preferred size is big enough (200px) that it is unlikely + // to get an overlap. + + function test_init() { + setupWarningsFilter(); + { + const sheet = createTemporaryObject(emptyComponent, null); + verify(sheet); + verify(!sheet.parent); + } + { + const sheet = createTemporaryObject(emptyComponent, this); + verify(sheet); + compare(sheet.parent, this); + } + { + const sheet = createTemporaryObject(viewContentWithImplicitSizeComponent, this); + verify(sheet); + verify(sheet.implicitWidth >= 100); + verify(sheet.implicitHeight >= 200); + } + { + const sheet = createTemporaryObject(viewContentWithPreferredSizeComponent, this); + verify(sheet); + verify(sheet.implicitWidth >= 100); + verify(sheet.implicitHeight >= 200); + } + } + + // Layout.preferred* takes precedence over implicit size + function test_sizeHintPrecedence() { + setupWarningsFilter(); + { + // first, make sure 100x200 sheet's total implicit size is smaller than 300x400 + const sheet = createTemporaryObject(itemContentComponent, this); + verify(sheet); + verify(sheet.implicitWidth >= 100 && sheet.implicitWidth < 300); + verify(sheet.implicitHeight >= 200 && sheet.implicitHeight < 400); + } + { + const sheet = createTemporaryObject(itemContentWithMixedSizesComponent, this); + verify(sheet); + verify(sheet.implicitWidth >= 300); + verify(sheet.implicitHeight >= 400); + } + { + const sheet = createTemporaryObject(viewContentWithMixedSizesComponent, this); + verify(sheet); + verify(sheet.implicitWidth >= 300); + verify(sheet.implicitHeight >= 400); + } + } +} diff --git a/autotests/tst_overlayzstacking.qml b/autotests/tst_overlayzstacking.qml new file mode 100644 index 0000000..7a764bf --- /dev/null +++ b/autotests/tst_overlayzstacking.qml @@ -0,0 +1,237 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import QtTest +import org.kde.kirigami as Kirigami + +TestCase { + id: root + + name: "OverlayZStackingTest" + visible: true + when: windowShown + + width: 300 + height: 300 + + // Layers are not set + Component { + id: defaultComponent + T.Popup { + z: Kirigami.OverlayZStacking.z + } + } + Component { + id: defaultDrawerComponent + T.Drawer { + z: Kirigami.OverlayZStacking.z + } + } + Component { + id: defaultDialogComponent + T.Dialog { + z: Kirigami.OverlayZStacking.z + } + } + Component { + id: defaultMenuComponent + T.Menu { + z: Kirigami.OverlayZStacking.z + } + } + Component { + id: defaultToolTipComponent + T.ToolTip { + z: Kirigami.OverlayZStacking.z + } + } + + function test_defaultLayers() { + let popup = null; + + popup = createTemporaryObject(defaultComponent, this); + verify(popup); + compare(popup.Kirigami.OverlayZStacking.layer, Kirigami.OverlayZStacking.DefaultLowest); + compare(popup.z, 0); + + popup = createTemporaryObject(defaultDrawerComponent, this); + compare(popup.Kirigami.OverlayZStacking.layer, Kirigami.OverlayZStacking.Drawer); + verify(popup); + compare(popup.z, 100); + + popup = createTemporaryObject(defaultDialogComponent, this); + verify(popup); + compare(popup.Kirigami.OverlayZStacking.layer, Kirigami.OverlayZStacking.Dialog); + compare(popup.z, 300); + + popup = createTemporaryObject(defaultMenuComponent, this); + verify(popup); + compare(popup.Kirigami.OverlayZStacking.layer, Kirigami.OverlayZStacking.Menu); + compare(popup.z, 400); + + popup = createTemporaryObject(defaultToolTipComponent, this); + verify(popup); + compare(popup.Kirigami.OverlayZStacking.layer, Kirigami.OverlayZStacking.ToolTip); + compare(popup.z, 600); + } + + // Layers are set + Component { + id: drawerComponent + T.Drawer { + Kirigami.OverlayZStacking.layer: Kirigami.OverlayZStacking.Drawer + z: Kirigami.OverlayZStacking.z + } + } + Component { + id: fullScreenComponent + T.Popup { + Kirigami.OverlayZStacking.layer: Kirigami.OverlayZStacking.FullScreen + z: Kirigami.OverlayZStacking.z + } + } + Component { + id: dialogComponent + T.Dialog { + Kirigami.OverlayZStacking.layer: Kirigami.OverlayZStacking.Dialog + z: Kirigami.OverlayZStacking.z + } + } + Component { + id: menuComponent + T.Menu { + Kirigami.OverlayZStacking.layer: Kirigami.OverlayZStacking.Menu + z: Kirigami.OverlayZStacking.z + } + } + Component { + id: notificationComponent + T.ToolTip { + Kirigami.OverlayZStacking.layer: Kirigami.OverlayZStacking.Notification + z: Kirigami.OverlayZStacking.z + } + } + Component { + id: toolTipComponent + T.ToolTip { + Kirigami.OverlayZStacking.layer: Kirigami.OverlayZStacking.ToolTip + z: Kirigami.OverlayZStacking.z + } + } + + function createWithLayers() { + const drawer = createTemporaryObject(drawerComponent, this); + verify(drawer); + const fullScreen = createTemporaryObject(fullScreenComponent, this); + verify(fullScreen); + const dialog = createTemporaryObject(dialogComponent, this); + verify(dialog); + const menu = createTemporaryObject(menuComponent, this); + verify(menu); + const notification = createTemporaryObject(notificationComponent, this); + verify(notification); + const toolTip = createTemporaryObject(toolTipComponent, this); + verify(toolTip); + return ({ + drawer, + fullScreen, + dialog, + menu, + notification, + toolTip, + }); + } + + function test_stackingNaturalOrder() { + const all = createWithLayers(); + const { drawer, fullScreen, dialog, menu, notification, toolTip } = all; + + drawer .parent = this; + fullScreen .parent = drawer .contentItem; + dialog .parent = fullScreen .contentItem; + menu .parent = dialog .contentItem; + notification.parent = menu .contentItem; + toolTip .parent = notification .contentItem; + + drawer.open(); + compare(drawer.z, 100); + fullScreen.open(); + compare(fullScreen.z, 200); + dialog.open(); + compare(dialog.z, 300); + menu.open(); + compare(menu.z, 400); + notification.open(); + compare(notification.z, 500); + toolTip.open(); + compare(toolTip.z, 600); + } + + function test_stackingReverseOrder() { + const all = createWithLayers(); + const { drawer, fullScreen, dialog, menu, notification, toolTip } = all; + + toolTip .parent = this; + notification.parent = toolTip .contentItem; + menu .parent = notification.contentItem; + dialog .parent = menu .contentItem; + fullScreen .parent = dialog .contentItem; + drawer .parent = fullScreen .contentItem; + + toolTip.open(); + compare(toolTip.z, 600); + notification.open(); + compare(notification.z, 601); + menu.open(); + compare(menu.z, 602); + dialog.open(); + compare(dialog.z, 603); + fullScreen.open(); + compare(fullScreen.z, 604); + drawer.open(); + compare(drawer.z, 605); + } + + Component { + id: spyComponent + SignalSpy {} + } + + function test_parentChangesZ() { + const parent = createTemporaryObject(defaultComponent, this, { parent: this }); + const child = createTemporaryObject(defaultComponent, this); + const spy = createTemporaryObject(spyComponent, this, { + target: child, + signalName: "zChanged", + }); + verify(spy.valid); + compare(spy.count, 0); + compare(parent.z, 0); + + child.parent = parent.contentItem; + compare(spy.count, 1); + compare(child.z, 1); + + parent.z = 42; + compare(spy.count, 2); + compare(child.z, 43); + + spy.clear(); + parent.open(); + child.open(); + // deferred signal + parent.z = 9000; + compare(spy.count, 0); + compare(child.z, 43); + + child.close(); + compare(spy.count, 1); + compare(child.z, 9001); + } +} diff --git a/autotests/tst_padding.qml b/autotests/tst_padding.qml new file mode 100644 index 0000000..08e0c6e --- /dev/null +++ b/autotests/tst_padding.qml @@ -0,0 +1,289 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import QtTest + +TestCase { + id: root + + name: "PaddingTest" + visible: true + when: windowShown + + width: 300 + height: 300 + + Component { + id: spyComponent + SignalSpy {} + } + + Component { + id: emptyComponent + Kirigami.Padding {} + } + + function test_empty() { + const item = createTemporaryObject(emptyComponent, this); + verify(item); + verify(!item.contentItem); + + compare(item.implicitWidth, 0); + compare(item.implicitHeight, 0); + compare(item.width, 0); + compare(item.height, 0); + compare(item.implicitContentWidth, 0); + compare(item.implicitContentHeight, 0); + + compare(item.padding, 0); + compare(item.topPadding, 0); + compare(item.leftPadding, 0); + compare(item.rightPadding, 0); + compare(item.bottomPadding, 0); + compare(item.verticalPadding, 0); + compare(item.horizontalPadding, 0); + compare(item.baselineOffset, 0); + + item.topPadding = 1; + item.leftPadding = 2; + item.rightPadding = 30; + item.bottomPadding = 40; + verify(waitForRendering(item)) + + compare(item.implicitWidth, 32); + compare(item.implicitHeight, 41); + } + + function test_paddingInheritance() { + const item = createTemporaryObject(emptyComponent, this); + verify(item); + + const paddingSpy = createTemporaryObject(spyComponent, this, { target: item, signalName: "paddingChanged" }); + verify(paddingSpy); + const topPaddingSpy = createTemporaryObject(spyComponent, this, { target: item, signalName: "topPaddingChanged" }); + verify(topPaddingSpy); + const leftPaddingSpy = createTemporaryObject(spyComponent, this, { target: item, signalName: "leftPaddingChanged" }); + verify(leftPaddingSpy); + const rightPaddingSpy = createTemporaryObject(spyComponent, this, { target: item, signalName: "rightPaddingChanged" }); + verify(rightPaddingSpy); + const bottomPaddingSpy = createTemporaryObject(spyComponent, this, { target: item, signalName: "bottomPaddingChanged" }); + verify(bottomPaddingSpy); + const verticalPaddingSpy = createTemporaryObject(spyComponent, this, { target: item, signalName: "verticalPaddingChanged" }); + verify(verticalPaddingSpy); + const horizontalPaddingSpy = createTemporaryObject(spyComponent, this, { target: item, signalName: "horizontalPaddingChanged" }); + verify(horizontalPaddingSpy); + + compare(item.padding, 0); + compare(item.topPadding, 0); + compare(item.leftPadding, 0); + compare(item.rightPadding, 0); + compare(item.bottomPadding, 0); + compare(item.verticalPadding, 0); + compare(item.horizontalPadding, 0); + compare(item.baselineOffset, 0); + + item.padding = 1; + compare(paddingSpy.count, 1), paddingSpy.clear(); + compare(topPaddingSpy.count, 1), topPaddingSpy.clear(); + compare(leftPaddingSpy.count, 1), leftPaddingSpy.clear(); + compare(rightPaddingSpy.count, 1), rightPaddingSpy.clear(); + compare(bottomPaddingSpy.count, 1), bottomPaddingSpy.clear(); + compare(verticalPaddingSpy.count, 1), verticalPaddingSpy.clear(); + compare(horizontalPaddingSpy.count, 1), horizontalPaddingSpy.clear(); + + compare(item.padding, 1); + compare(item.topPadding, 1); + compare(item.leftPadding, 1); + compare(item.rightPadding, 1); + compare(item.bottomPadding, 1); + compare(item.verticalPadding, 1); + compare(item.horizontalPadding, 1); + compare(item.baselineOffset, 0); + + item.leftPadding = 2; + compare(leftPaddingSpy.count, 1), leftPaddingSpy.clear(); + + compare(item.padding, 1); + compare(item.topPadding, 1); + compare(item.leftPadding, 2); + compare(item.rightPadding, 1); + compare(item.bottomPadding, 1); + + item.horizontalPadding = 3; + compare(leftPaddingSpy.count, 0); + compare(rightPaddingSpy.count, 1), rightPaddingSpy.clear(); + compare(horizontalPaddingSpy.count, 1), horizontalPaddingSpy.clear(); + + compare(item.padding, 1); + compare(item.topPadding, 1); + compare(item.leftPadding, 2); + compare(item.rightPadding, 3); + compare(item.bottomPadding, 1); + compare(item.horizontalPadding, 3); + compare(item.verticalPadding, 1); + + item.verticalPadding = 4; + verify(waitForRendering(item)) + compare(topPaddingSpy.count, 1), topPaddingSpy.clear(); + compare(bottomPaddingSpy.count, 1), bottomPaddingSpy.clear(); + compare(verticalPaddingSpy.count, 1), verticalPaddingSpy.clear(); + + compare(item.padding, 1); + compare(item.topPadding, 4); + compare(item.leftPadding, 2); + compare(item.rightPadding, 3); + compare(item.bottomPadding, 4); + compare(item.horizontalPadding, 3); + compare(item.verticalPadding, 4); + + item.topPadding = 5; + verify(waitForRendering(item)) + compare(topPaddingSpy.count, 1), topPaddingSpy.clear(); + + compare(item.padding, 1); + compare(item.topPadding, 5); + compare(item.leftPadding, 2); + compare(item.rightPadding, 3); + compare(item.bottomPadding, 4); + compare(item.horizontalPadding, 3); + compare(item.verticalPadding, 4); + + item.bottomPadding = 6; + verify(waitForRendering(item)) + compare(bottomPaddingSpy.count, 1), bottomPaddingSpy.clear(); + + compare(item.padding, 1); + compare(item.topPadding, 5); + compare(item.leftPadding, 2); + compare(item.rightPadding, 3); + compare(item.bottomPadding, 6); + compare(item.horizontalPadding, 3); + compare(item.verticalPadding, 4); + + item.bottomPadding = undefined; + compare(bottomPaddingSpy.count, 1), bottomPaddingSpy.clear(); + + compare(item.padding, 1); + compare(item.topPadding, 5); + compare(item.leftPadding, 2); + compare(item.rightPadding, 3); + compare(item.bottomPadding, 4); + compare(item.horizontalPadding, 3); + compare(item.verticalPadding, 4); + + item.verticalPadding = undefined; + compare(topPaddingSpy.count, 0); + compare(bottomPaddingSpy.count, 1), bottomPaddingSpy.clear(); + compare(verticalPaddingSpy.count, 1), verticalPaddingSpy.clear(); + + compare(item.padding, 1); + compare(item.topPadding, 5); + compare(item.leftPadding, 2); + compare(item.rightPadding, 3); + compare(item.bottomPadding, 1); + compare(item.horizontalPadding, 3); + compare(item.verticalPadding, 1); + + item.rightPadding = 7; + verify(waitForRendering(item)) + compare(rightPaddingSpy.count, 1), rightPaddingSpy.clear(); + + compare(item.padding, 1); + compare(item.topPadding, 5); + compare(item.leftPadding, 2); + compare(item.rightPadding, 7); + compare(item.bottomPadding, 1); + compare(item.horizontalPadding, 3); + compare(item.verticalPadding, 1); + } + + Component { + id: sizedComponent + Kirigami.Padding { + topPadding: 1 + leftPadding: 2 + rightPadding: 30 + bottomPadding: 40 + + contentItem: Rectangle { + color: "#1EA8F7" + implicitWidth: 100 + implicitHeight: 200 + } + } + } + + function test_fixedSizeComponent() { + const item = createTemporaryObject(sizedComponent, this); + verify(item); + verify(waitForRendering(item)) + + compare(item.implicitWidth, 132); + compare(item.implicitHeight, 241); + + const widthSpy = createTemporaryObject(spyComponent, this, { target: item, signalName: "implicitWidthChanged" }); + const heightSpy = createTemporaryObject(spyComponent, this, { target: item, signalName: "implicitHeightChanged" }); + + item.contentItem.implicitWidth = 1000; + verify(waitForRendering(item)) + compare(widthSpy.count, 1), widthSpy.clear(); + compare(heightSpy.count, 0); + + item.contentItem.implicitHeight = 2000; + verify(waitForRendering(item)) + compare(widthSpy.count, 0); + compare(heightSpy.count, 1), heightSpy.clear(); + + compare(item.implicitWidth, 1032); + compare(item.implicitHeight, 2041); + + item.width = 100; + item.height = 200; + verify(waitForRendering(item)) + + compare(item.contentItem.width, 100 - 2 - 30); + compare(item.contentItem.height, 200 - 1 - 40); + } + + Component { + id: dynamicComponent + Kirigami.Padding { + topPadding: 1 + leftPadding: 2 + rightPadding: 30 + bottomPadding: 40 + } + } + + Component { + id: contentComponent + Rectangle { + color: "#1EA8F7" + implicitWidth: 100 + implicitHeight: 200 + } + } + + function test_dynamicComponent() { + const item = createTemporaryObject(dynamicComponent, this); + verify(item); + verify(waitForRendering(item)) + const content = createTemporaryObject(contentComponent, this); + verify(content); + + compare(item.width, 32); + compare(item.height, 41); + + item.contentItem = content; + verify(waitForRendering(item)) + + compare(item.implicitWidth, 132); + compare(item.implicitHeight, 241); + } +} diff --git a/autotests/tst_pageStackAttached.qml b/autotests/tst_pageStackAttached.qml new file mode 100644 index 0000000..0dc1b66 --- /dev/null +++ b/autotests/tst_pageStackAttached.qml @@ -0,0 +1,290 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC +import QtTest +import org.kde.kirigami as Kirigami + +TestCase { + id: testCase + name: "PageStackAttachedTest" + + width: 400 + height: 400 + visible: true + + when: windowShown + + Kirigami.ApplicationWindow { + id: mainWindow + width: 480 + height: 360 + } + + Component { + id: samplePage + Kirigami.Page { + property Item outerStack: Kirigami.PageStack.pageStack + property alias innerRect: rect + Rectangle { + id: rect + color: "green" + property Item stackFromChild: Kirigami.PageStack.pageStack + } + } + } + + Component { + id: pageWithInnerStack + Kirigami.Page { + property Item stack: Kirigami.PageStack.pageStack + property alias subStack: stackView + QQC.StackView { + id: stackView + } + } + } + + Component { + id: pageInLayer + Kirigami.Page { + property Item outerStack: Kirigami.PageStack.pageStack + property alias innerRect: rect + Rectangle { + id: rect + color: "blue" + property Item stackFromChild: Kirigami.PageStack.pageStack + } + } + } + + SignalSpy { + id: spyStackChanged + signalName: "pageStackChanged" + } + + + function initTestCase(): void { + mainWindow.show() + } + + function cleanupTestCase(): void { + mainWindow.close() + } + + function init(): void { + mainWindow.pageStack.clear() + spyStackChanged.clear() + } + + function test_accessPageRow(): void { + compare(mainWindow.pageStack.depth, 0) + mainWindow.pageStack.push(samplePage) + compare(mainWindow.pageStack.depth, 1) + + let pageRowFirstPage = mainWindow.pageStack.items[0] + + compare(pageRowFirstPage.outerStack, mainWindow.pageStack) + compare(pageRowFirstPage.innerRect.stackFromChild, mainWindow.pageStack) + } + + function test_accessInnerStack(): void { + compare(mainWindow.pageStack.depth, 0) + mainWindow.pageStack.push(pageWithInnerStack) + compare(mainWindow.pageStack.depth, 1) + + let pageRowFirstPage = mainWindow.pageStack.items[0] + compare(pageRowFirstPage.stack, mainWindow.pageStack) + + pageRowFirstPage.subStack.push(samplePage) + + compare(pageRowFirstPage.subStack.currentItem.outerStack, pageRowFirstPage.subStack) + compare(pageRowFirstPage.subStack.currentItem.innerRect.stackFromChild, pageRowFirstPage.subStack) + } + + function test_accessLayersStack(): void { + compare(mainWindow.pageStack.depth, 0) + mainWindow.pageStack.push(samplePage) + compare(mainWindow.pageStack.depth, 1) + + mainWindow.pageStack.layers.push(pageInLayer) + + let pageRowFirstPage = mainWindow.pageStack.items[0] + compare(pageRowFirstPage.outerStack, mainWindow.pageStack) + compare(pageRowFirstPage.innerRect.stackFromChild, mainWindow.pageStack) + + let layer1Page = mainWindow.pageStack.layers.currentItem + compare(layer1Page.outerStack, mainWindow.pageStack.layers) + compare(layer1Page.innerRect.stackFromChild, mainWindow.pageStack.layers) + } + + function test_changeParent(): void { + compare(mainWindow.pageStack.depth, 0) + mainWindow.pageStack.push(samplePage) + compare(mainWindow.pageStack.depth, 1) + + let pageRowFirstPage = mainWindow.pageStack.items[0] + + compare(pageRowFirstPage.outerStack, mainWindow.pageStack) + compare(pageRowFirstPage.innerRect.stackFromChild, mainWindow.pageStack) + + mainWindow.pageStack.push(pageWithInnerStack) + compare(mainWindow.pageStack.depth, 2) + + let pageRowSecondPage = mainWindow.pageStack.items[1] + + compare(pageRowSecondPage.stack, mainWindow.pageStack) + + spyStackChanged.target = pageRowFirstPage.innerRect.Kirigami.PageStack + // First we make sure the internal stack has an attached property created + verify(pageRowSecondPage.subStack.Kirigami.PageStack.pageStack) + pageRowSecondPage.subStack.push(pageRowFirstPage.innerRect) + tryCompare(spyStackChanged, "count", 1) + compare(pageRowFirstPage.innerRect.parent, pageRowSecondPage.subStack) + compare(pageRowFirstPage.innerRect.stackFromChild, pageRowSecondPage.subStack); + } + + function test_changeParent_attachedNotExisting(): void { + // Here will reparent to a stackview in a page which + // doesn't have a stackattached created yet + compare(mainWindow.pageStack.depth, 0) + mainWindow.pageStack.push(samplePage) + compare(mainWindow.pageStack.depth, 1) + + let pageRowFirstPage = mainWindow.pageStack.items[0] + + compare(pageRowFirstPage.outerStack, mainWindow.pageStack) + compare(pageRowFirstPage.innerRect.stackFromChild, mainWindow.pageStack) + + mainWindow.pageStack.push(pageWithInnerStack) + compare(mainWindow.pageStack.depth, 2) + + let pageRowSecondPage = mainWindow.pageStack.items[1] + + compare(pageRowSecondPage.stack, mainWindow.pageStack) + + spyStackChanged.target = pageRowFirstPage.innerRect.Kirigami.PageStack + pageRowSecondPage.subStack.push(pageRowFirstPage.innerRect) + tryCompare(spyStackChanged, "count", 0) + // access the pagestack, ensuring it exists + verify(pageRowSecondPage.subStack.Kirigami.PageStack.pageStack) + // Now pageRowFirstPage.innerRect changed + tryCompare(spyStackChanged, "count", 1) + + compare(pageRowFirstPage.innerRect.parent, pageRowSecondPage.subStack) + compare(pageRowFirstPage.innerRect.stackFromChild, pageRowSecondPage.subStack); + } + + function test_attachedPushPopOnPageRow(): void { + compare(mainWindow.pageStack.depth, 0) + mainWindow.pageStack.push(samplePage) + compare(mainWindow.pageStack.depth, 1) + + let pageRowFirstPage = mainWindow.pageStack.items[0] + pageRowFirstPage.Kirigami.PageStack.push(samplePage); + compare(mainWindow.pageStack.depth, 2) + + let pageRowSecondPage = mainWindow.pageStack.items[1] + pageRowSecondPage.innerRect.Kirigami.PageStack.push(samplePage) + compare(mainWindow.pageStack.depth, 3) + + pageRowFirstPage.Kirigami.PageStack.pop(pageRowFirstPage) + compare(mainWindow.pageStack.depth, 1) + } + + function test_attachedPushPopOnStackView(): void { + compare(mainWindow.pageStack.depth, 0) + mainWindow.pageStack.push(pageWithInnerStack) + compare(mainWindow.pageStack.depth, 1) + + let pageRowFirstPage = mainWindow.pageStack.items[0] + pageRowFirstPage.subStack.push(samplePage) + compare(pageRowFirstPage.subStack.depth, 1) + + let subStackFirstPage = pageRowFirstPage.subStack.currentItem + + subStackFirstPage.Kirigami.PageStack.push(samplePage) + compare(pageRowFirstPage.subStack.depth, 2) + subStackFirstPage.Kirigami.PageStack.push(samplePage) + compare(pageRowFirstPage.subStack.depth, 3) + + subStackFirstPage.Kirigami.PageStack.pop(subStackFirstPage) + compare(pageRowFirstPage.subStack.depth, 1) + } + + function test_attachedReplaceOnPageRow(): void { + compare(mainWindow.pageStack.depth, 0) + mainWindow.pageStack.push(samplePage) + compare(mainWindow.pageStack.depth, 1) + + let pageRowFirstPage = mainWindow.pageStack.items[0] + pageRowFirstPage.Kirigami.PageStack.push(samplePage); + compare(mainWindow.pageStack.depth, 2) + + let pageRowSecondPage = mainWindow.pageStack.items[1] + pageRowSecondPage.innerRect.Kirigami.PageStack.replace(pageWithInnerStack) + compare(mainWindow.pageStack.depth, 2) + pageRowSecondPage = mainWindow.pageStack.items[1] + verify(pageRowSecondPage.hasOwnProperty("subStack")) + } + + function test_attachedReplaceOnStackView(): void { + compare(mainWindow.pageStack.depth, 0) + mainWindow.pageStack.push(pageWithInnerStack) + compare(mainWindow.pageStack.depth, 1) + + let pageRowFirstPage = mainWindow.pageStack.items[0] + pageRowFirstPage.subStack.push(samplePage) + compare(pageRowFirstPage.subStack.depth, 1) + + let subStackFirstPage = pageRowFirstPage.subStack.currentItem + + subStackFirstPage.Kirigami.PageStack.push(samplePage) + compare(pageRowFirstPage.subStack.depth, 2) + subStackFirstPage.Kirigami.PageStack.replace(pageWithInnerStack) + compare(pageRowFirstPage.subStack.depth, 2) + + verify(pageRowFirstPage.subStack.currentItem.hasOwnProperty("subStack")) + } + + function test_attachedClearOnPageRow(): void { + compare(mainWindow.pageStack.depth, 0) + mainWindow.pageStack.push(samplePage) + compare(mainWindow.pageStack.depth, 1) + + let pageRowFirstPage = mainWindow.pageStack.items[0] + pageRowFirstPage.Kirigami.PageStack.push(samplePage); + compare(mainWindow.pageStack.depth, 2) + + let pageRowSecondPage = mainWindow.pageStack.items[1] + pageRowSecondPage.innerRect.Kirigami.PageStack.push(samplePage) + compare(mainWindow.pageStack.depth, 3) + + pageRowFirstPage.Kirigami.PageStack.clear() + compare(mainWindow.pageStack.depth, 0) + } + + function test_attachedClearOnStackView(): void { + compare(mainWindow.pageStack.depth, 0) + mainWindow.pageStack.push(pageWithInnerStack) + compare(mainWindow.pageStack.depth, 1) + + let pageRowFirstPage = mainWindow.pageStack.items[0] + pageRowFirstPage.subStack.push(samplePage) + compare(pageRowFirstPage.subStack.depth, 1) + + let subStackFirstPage = pageRowFirstPage.subStack.currentItem + + subStackFirstPage.Kirigami.PageStack.push(samplePage) + compare(pageRowFirstPage.subStack.depth, 2) + subStackFirstPage.Kirigami.PageStack.push(samplePage) + compare(pageRowFirstPage.subStack.depth, 3) + + subStackFirstPage.Kirigami.PageStack.clear() + compare(pageRowFirstPage.subStack.depth, 0) + } +} diff --git a/autotests/tst_pagerow.qml b/autotests/tst_pagerow.qml new file mode 100644 index 0000000..286636b --- /dev/null +++ b/autotests/tst_pagerow.qml @@ -0,0 +1,164 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Window +import org.kde.kirigami as Kirigami +import QtTest + +TestCase { + id: testCase + width: 400 + height: 400 + name: "GoBack" + + function applicationWindow() { return mainWindow; } + + Kirigami.ApplicationWindow { + id: mainWindow + width: 480 + height: 360 + pageStack.initialPage: Kirigami.Page { + Rectangle { + anchors.fill: parent + color: "green" + } + } + } + + Component { + id: randomPage + Kirigami.Page { + Rectangle { + anchors.fill: parent + color: "red" + } + } + } + + SignalSpy { + id: spyCurrentIndex + target: mainWindow.pageStack + signalName: "currentIndexChanged" + } + + SignalSpy { + id: spyActive + target: mainWindow + signalName: "activeChanged" + } + + function initTestCase() { + mainWindow.show() + } + + function cleanupTestCase() { + mainWindow.close() + } + + function init() { + mainWindow.pageStack.clear() + spyActive.clear() + spyCurrentIndex.clear() + } + + function test_pop() { + compare(mainWindow.pageStack.depth, 0) + mainWindow.pageStack.push(randomPage) + compare(mainWindow.pageStack.depth, 1) + mainWindow.pageStack.pop() + compare(mainWindow.pageStack.depth, 0) + } + + function test_goBack() { + compare(mainWindow.pageStack.depth, 0) + compare(mainWindow.pageStack.currentIndex, -1) + + let page = mainWindow.pageStack.push(randomPage) + print(page) + tryCompare(spyCurrentIndex, "count", 1) + compare(mainWindow.pageStack.depth, 1) + compare(mainWindow.pageStack.currentIndex, 0) + + page = mainWindow.pageStack.push(randomPage) + print(page) + tryCompare(spyCurrentIndex, "count", 2) + compare(mainWindow.pageStack.depth, 2) + + compare(mainWindow.pageStack.depth, 2) + compare(mainWindow.pageStack.currentIndex, 1) + + spyActive.clear() + mainWindow.requestActivate() + spyCurrentIndex.clear() + if (!mainWindow.active) + spyActive.wait() + verify(mainWindow.active) + keyClick(Qt.Key_Left, Qt.AltModifier) + + spyCurrentIndex.wait() + + compare(mainWindow.pageStack.depth, 2) + compare(mainWindow.pageStack.currentIndex, 0) + compare(spyCurrentIndex.count, 1) + mainWindow.pageStack.pop() + compare(mainWindow.pageStack.depth, 1) + } + + function test_pushForgettingHistory() { + let page = mainWindow.pageStack.push(randomPage) + page.title = "P1" + tryCompare(spyCurrentIndex, "count", 1) + page = mainWindow.pageStack.push(randomPage) + page.title = "P2" + tryCompare(spyCurrentIndex, "count", 2) + page = mainWindow.pageStack.push(randomPage) + page.title = "P3" + tryCompare(spyCurrentIndex, "count", 3) + + compare(mainWindow.pageStack.depth, 3) + compare(mainWindow.pageStack.currentIndex, 2) + mainWindow.pageStack.currentIndex = 0 + page = mainWindow.pageStack.push(randomPage) + compare(mainWindow.pageStack.depth, 2) + compare(mainWindow.pageStack.items[0].title, "P1") + // This is the newly pushed page, everything else went away + compare(mainWindow.pageStack.items[1].title, "") + compare(mainWindow.pageStack.items[1], page) + } + + property int destructions: 0 + Component { + id: destroyedPage + Kirigami.Page { + id: page + Rectangle { + anchors.fill: parent + color: "blue" + Component.onDestruction: { + testCase.destructions++ + } + } + } + } + SignalSpy { + id: spyDestructions + target: testCase + signalName: "destructionsChanged" + } + function test_clearPages() { + mainWindow.pageStack.push(destroyedPage) + mainWindow.pageStack.push(destroyedPage) + mainWindow.pageStack.push(destroyedPage) + compare(mainWindow.pageStack.depth, 3) + mainWindow.pageStack.clear() + + compare(mainWindow.pageStack.depth, 0) + spyDestructions.wait() + compare(testCase.destructions, 3) + } +} diff --git a/autotests/tst_placeholdermessage.qml b/autotests/tst_placeholdermessage.qml new file mode 100644 index 0000000..4741ad0 --- /dev/null +++ b/autotests/tst_placeholdermessage.qml @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import QtTest + +TestCase { + id: root + + name: "PlaceholderMessageTest" + visible: true + when: windowShown + + width: 300 + height: 300 + + Component { + id: placeholderMessageComponent + Kirigami.PlaceholderMessage { + id: message + + anchors.centerIn: parent + width: parent.width - (Kirigami.Units.largeSpacing * 4) + + readonly property SignalSpy linkHoveredSpy: SignalSpy { + target: message + signalName: "linkHovered" + } + readonly property SignalSpy linkActivatedSpy: SignalSpy { + target: message + signalName: "linkActivated" + } + } + } + + Component { + id: helpfulMessageComponent + Kirigami.PlaceholderMessage { + id: message + + property int count: 0 + + anchors.centerIn: parent + width: parent.width - (Kirigami.Units.largeSpacing * 4) + + helpfulAction: QQC2.Action { + onTriggered: { + message.count += 1 + } + } + } + } + + function hoverAll(item: Item, /*predicate: (x, y) => bool*/ until) { + for (let x = 0; x < item.width; x += 10) { + for (let y = 0; y < item.height; y += 10) { + mouseMove(item, x, y); + if (until(x, y)) { + return Qt.point(x, y); + } + } + } + return null; + } + + function test_link() { + skip("finding links by sweeping with the mouse cursor all over the place seems very unreliable, especially on FreeBSD and Windows TODO: find a better way to find links") + const href = "some"; + const message = createTemporaryObject(placeholderMessageComponent, this, { + text: "Attention!", + explanation: `link`, + }); + verify(message); + verify(message.linkHoveredSpy.valid); + verify(message.linkActivatedSpy.valid); + + const point = hoverAll(message, (x, y) => message.hoveredLink === href); + verify(point !== null); + compare(message.linkHoveredSpy.count, 1); + const hoveredLink = message.linkHoveredSpy.signalArguments[0][0]; + compare(hoveredLink, href); + + mouseClick(message, point.x, point.y); + compare(message.linkActivatedSpy.count, 1); + const activatedLink = message.linkActivatedSpy.signalArguments[0][0]; + compare(activatedLink, href); + } + + function test_action() { + const message = createTemporaryObject(helpfulMessageComponent, this, { + text: "Attention!", + }); + const point = hoverAll(message, (x, y) => { + mouseClick(message, x, y); + return message.count > 0; + }); + verify(point !== null); + compare(message.count, 1); + } + + function test_disabled_action() { + const message = createTemporaryObject(helpfulMessageComponent, this, { + text: "Attention!", + }); + message.helpfulAction.enabled = false; + const point = hoverAll(message, (x, y) => { + mouseClick(message, x, y); + return message.count > 0; + }); + verify(point === null); + compare(message.count, 0); + } + + function test_null_action() { + const message = createTemporaryObject(helpfulMessageComponent, this, { + text: "Attention!", + helpfulAction: null, + }); + const point = hoverAll(message, (x, y) => { + mouseClick(message, x, y); + return message.count > 0; + }); + verify(point === null); + compare(message.count, 0); + } +} diff --git a/autotests/tst_sceneposition.qml b/autotests/tst_sceneposition.qml new file mode 100644 index 0000000..140f473 --- /dev/null +++ b/autotests/tst_sceneposition.qml @@ -0,0 +1,234 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import QtTest + +TestCase { + id: root + + name: "ScenePositionTest" + visible: true + when: windowShown + + width: 300 + height: 300 + + Item { + id: anchorsFillItem + anchors.fill: parent + + Item { + id: itemA + x: 50 + y: 100 + } + + Item { + id: itemB + x: 150 + y: 200 + + Item { + id: itemB1 + x: 25 + y: 50 + } + } + + Item { + id: itemF + x: 3.5 + y: 6.25 + + Item { + id: itemF1 + x: -0.25 + y: 1.125 + } + } + + Item { + id: itemPlaceholder + x: 56 + y: 78 + + property Item __reparentedItem: Item { + id: reparentedItem + parent: null + x: 12 + y: 34 + } + } + + Rectangle { + id: itemTransform + + x: 100 + y: 200 + width: 10 + height: 10 + color: "red" + + scale: 2.0 + rotation: 30 + transform: [ + Rotation { + angle: 30 + }, + Scale { + xScale: 2.0 + yScale: 3.0 + }, + Translate { + x: 12 + y: 34 + }, + Matrix4x4 { + property real a: Math.PI / 4 + matrix: Qt.matrix4x4(Math.cos(a), -Math.sin(a), 0, 0, + Math.sin(a), Math.cos(a), 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1) + } + ] + } + } + + function test_root() { + compare(root.Kirigami.ScenePosition.x, 0); + compare(root.Kirigami.ScenePosition.y, 0); + + compare(anchorsFillItem.Kirigami.ScenePosition.x, 0); + compare(anchorsFillItem.Kirigami.ScenePosition.y, 0); + } + + function test_not_nested() { + compare(itemA.Kirigami.ScenePosition.x, itemA.x); + compare(itemA.Kirigami.ScenePosition.y, itemA.y); + + compare(itemB.Kirigami.ScenePosition.x, itemB.x); + compare(itemB.Kirigami.ScenePosition.y, itemB.y); + } + + function test_nested() { + compare(itemB1.Kirigami.ScenePosition.x, itemB.x + itemB1.x); + compare(itemB1.Kirigami.ScenePosition.y, itemB.y + itemB1.y); + } + + function test_floating() { + compare(itemF1.Kirigami.ScenePosition.x, 3.25); + compare(itemF1.Kirigami.ScenePosition.y, 7.375); + } + + function test_reparented() { + reparentedItem.parent = null; + compare(reparentedItem.Kirigami.ScenePosition.x, reparentedItem.x); + compare(reparentedItem.Kirigami.ScenePosition.y, reparentedItem.y); + + itemPlaceholder.x = 56; + itemPlaceholder.y = 78; + reparentedItem.parent = itemPlaceholder; + compare(reparentedItem.Kirigami.ScenePosition.x, itemPlaceholder.x + reparentedItem.x); + compare(reparentedItem.Kirigami.ScenePosition.y, itemPlaceholder.y + reparentedItem.y); + + itemPlaceholder.x += 10; + itemPlaceholder.y += 20; + compare(reparentedItem.Kirigami.ScenePosition.x, itemPlaceholder.x + reparentedItem.x); + compare(reparentedItem.Kirigami.ScenePosition.y, itemPlaceholder.y + reparentedItem.y); + } + + function test_transform() { + // transformations are not supported by ScenePosition + compare(itemTransform.Kirigami.ScenePosition.x, itemTransform.x); + compare(itemTransform.Kirigami.ScenePosition.y, itemTransform.y); + } + + Item { + id: oldParent + Item { + id: reparentedChildItem + } + } + Item { + id: newParent + } + + SignalSpy { + id: reparentingXSpy + target: reparentedChildItem.Kirigami.ScenePosition + signalName: "xChanged" + } + SignalSpy { + id: reparentingYSpy + target: reparentedChildItem.Kirigami.ScenePosition + signalName: "yChanged" + } + + function test_disconnectOldAncestors() { + verify(reparentingXSpy.valid); + verify(reparentingYSpy.valid); + reparentingXSpy.clear(); + reparentingYSpy.clear(); + + // change position of the item itself + reparentedChildItem.x = 12; + compare(reparentingXSpy.count, 1); + compare(reparentingYSpy.count, 0); + reparentedChildItem.y = 34; + compare(reparentingXSpy.count, 1); + compare(reparentingYSpy.count, 1); + + // change position of the current parent + oldParent.x = 56; + compare(reparentedChildItem.Kirigami.ScenePosition.x, 12 + 56); + compare(reparentingXSpy.count, 2); + compare(reparentingYSpy.count, 1); + oldParent.y = 78; + compare(reparentedChildItem.Kirigami.ScenePosition.y, 34 + 78); + compare(reparentingXSpy.count, 2); + compare(reparentingYSpy.count, 2); + + // reparent + reparentedChildItem.parent = newParent; + compare(reparentingXSpy.count, 3); + compare(reparentingYSpy.count, 3); + compare(reparentedChildItem.Kirigami.ScenePosition.x, 12); + compare(reparentedChildItem.Kirigami.ScenePosition.y, 34); + + // change position of the new parent + newParent.x = 11; + compare(reparentedChildItem.Kirigami.ScenePosition.x, 12 + 11); + compare(reparentingXSpy.count, 4); + compare(reparentingYSpy.count, 3); + newParent.y = 22; + compare(reparentedChildItem.Kirigami.ScenePosition.y, 34 + 22); + compare(reparentingXSpy.count, 4); + compare(reparentingYSpy.count, 4); + + // change position of the item itself (again, but with new parent) + reparentedChildItem.x = 33; + compare(reparentedChildItem.Kirigami.ScenePosition.x, 33 + 11); + compare(reparentingXSpy.count, 5); + compare(reparentingYSpy.count, 4); + reparentedChildItem.y = 44; + compare(reparentedChildItem.Kirigami.ScenePosition.y, 44 + 22); + compare(reparentingXSpy.count, 5); + compare(reparentingYSpy.count, 5); + + // change position of the old parent, should not trigger signals of the item anymore + oldParent.x = 55; + compare(reparentedChildItem.Kirigami.ScenePosition.x, 33 + 11); + compare(reparentingXSpy.count, 5); + compare(reparentingYSpy.count, 5); + oldParent.y = 66; + compare(reparentedChildItem.Kirigami.ScenePosition.y, 44 + 22); + compare(reparentingXSpy.count, 5); + compare(reparentingYSpy.count, 5); + } +} diff --git a/autotests/tst_scrollablepage.qml b/autotests/tst_scrollablepage.qml new file mode 100644 index 0000000..a9a0ae8 --- /dev/null +++ b/autotests/tst_scrollablepage.qml @@ -0,0 +1,109 @@ +/* + SPDX-FileCopyrightText: 2023 Fushan Wen + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC +import QtTest +import org.kde.kirigami as Kirigami + +TestCase { + id: testCase + + width: 400 + height: 400 + name: "ScrollablePageTest" + visible: false + + SignalSpy { + id: currentItemChangedSpy + target: mainWindow.pageStack + signalName: "currentItemChanged" + } + + // Simulate KCM.ScrollViewKCM + Kirigami.ApplicationWindow { + id: mainWindow + width: 480 + height: 360 + pageStack.initialPage: Kirigami.Page { + id: root + function clickFirst() { + userList.itemAtIndex(0).clicked(); + } + property alias view: scroll.view + view: ListView { + id: userList + model: 1 + delegate: QQC.ItemDelegate { + id: delegate + width: ListView.view.width + text: String(index) + onClicked: { + mainWindow.pageStack.pop(); + mainWindow.pageStack.push(subPageComponent); + console.log("clicked") + } + } + } + QQC.ScrollView { + id: scroll + anchors.fill: parent + property Flickable view + activeFocusOnTab: false + contentItem: view + onViewChanged: { + view.parent = scroll; + } + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + } + } + } + + Component { + id: subPageComponent + Kirigami.ScrollablePage { + readonly property alias success: realNametextField.activeFocus + focus: true + ColumnLayout { + QQC.TextField { + id: realNametextField + focus: true + text: "This item should be focused by default" + } + QQC.TextField { + id: userNameField + text: "ddeeff" + } + } + } + } + + function initTestCase() { + mainWindow.show() + mainWindow.requestActivate(); + tryVerify(() => mainWindow.active); + } + + function cleanupTestCase() { + mainWindow.close() + } + + function test_defaultFocusInScrollablePage() { + mainWindow.pageStack.currentItem.clickFirst(); + if (!(mainWindow.pageStack.currentItem instanceof Kirigami.ScrollablePage)) { + currentItemChangedSpy.wait() + } + verify(mainWindow.pageStack.currentItem instanceof Kirigami.ScrollablePage) + // Consolidate the workaround for QTBUG-44043 + // https://invent.kde.org/frameworks/kirigami/-/commit/fd253ea5d9fa3f33411e54a596c021f51b5a102a + tryVerify(() => mainWindow.pageStack.currentItem.success) + } +} + + + diff --git a/autotests/tst_spellcheck.qml b/autotests/tst_spellcheck.qml new file mode 100644 index 0000000..c692586 --- /dev/null +++ b/autotests/tst_spellcheck.qml @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Templates as T +import QtTest +import org.kde.kirigami as Kirigami + +TestCase { + name: "SpellCheckAttachedTest" + visible: true + when: windowShown + + width: 500 + height: 500 + + Component { + id: emptyComponent + T.TextArea {} + } + + Component { + id: attachedComponent + T.TextArea { + Kirigami.SpellCheck.enabled: true + } + } + + function test_defaults() { + const area = createTemporaryObject(emptyComponent, this); + verify(area); + + verify(!area.Kirigami.SpellCheck.enabled); + area.Kirigami.SpellCheck.enabled = true; + verify(area.Kirigami.SpellCheck.enabled); + } + + function test_createAttached() { + const area = createTemporaryObject(attachedComponent, this); + verify(area); + + verify(area.Kirigami.SpellCheck.enabled); + } +} diff --git a/autotests/tst_theme.qml b/autotests/tst_theme.qml new file mode 100644 index 0000000..e24ce5f --- /dev/null +++ b/autotests/tst_theme.qml @@ -0,0 +1,378 @@ +/* + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls +import QtTest +import org.kde.kirigami as Kirigami + +TestCase { + id: testCase + name: "ThemeTest" + + width: 400 + height: 400 + visible: true + + when: windowShown + + TextMetrics { + id: textMetrics + } + + Component { + id: basic + + Rectangle { + color: Kirigami.Theme.backgroundColor + + property alias text: textItem + + Text { + id: textItem + color: Kirigami.Theme.textColor + font: Kirigami.Theme.defaultFont + } + } + } + + function test_basic() { + var item = createTemporaryObject(basic, testCase) + verify(item) + + compare(item.Kirigami.Theme.colorSet, Kirigami.Theme.Window) + compare(item.Kirigami.Theme.colorGroup, Kirigami.Theme.Active) + verify(item.Kirigami.Theme.inherit) + + compare(item.color, "#eff0f1") + compare(item.text.color, "#31363b") + compare(item.text.font.family, textMetrics.font.family) + } + + Component { + id: override + + Rectangle { + Kirigami.Theme.backgroundColor: "#ff0000" + color: Kirigami.Theme.backgroundColor + } + } + + function test_override() { + var item = createTemporaryObject(override, testCase) + verify(item) + + compare(item.color, "#ff0000") + + item.Kirigami.Theme.backgroundColor = "#00ff00" + + compare(item.color, "#00ff00") + + // Changing colorSet or colorGroup does not affect local overrides + item.Kirigami.Theme.colorSet = Kirigami.Theme.Complementary + item.Kirigami.Theme.colorGroup = Kirigami.Theme.Disabled + + compare(item.color, "#00ff00") + } + + Component { + id: inherit + + Rectangle { + // Ensure the root object we test with doesn't accidentally inherit + // from the parent test object. + Kirigami.Theme.inherit: false + + color: Kirigami.Theme.backgroundColor + + property alias child1: rect1 + property alias child2: rect2 + + Rectangle { + id: rect1 + color: Kirigami.Theme.backgroundColor + } + Rectangle { + id: rect2 + Kirigami.Theme.inherit: false + color: Kirigami.Theme.backgroundColor + } + } + } + + function test_inherit() { + var item = createTemporaryObject(inherit, testCase) + verify(item) + + // Default values are all the same + compare(item.color, "#eff0f1") + compare(item.child1.color, "#eff0f1") + compare(item.child2.color, "#eff0f1") + + // If we change the colorSet, the item that inherits gets updated, but + // the item that does not stays the same. + item.Kirigami.Theme.colorSet = Kirigami.Theme.View + + compare(item.color, "#fcfcfc") + compare(item.child1.color, "#fcfcfc") + compare(item.child2.color, "#eff0f1") + + // If we override a color, the item that inherits gets that color, while + // the item that does not ignores it. + item.Kirigami.Theme.backgroundColor = "#ff0000" + + compare(item.color, "#ff0000") + compare(item.child1.color, "#ff0000") + compare(item.child2.color, "#eff0f1") + + // If we change the color set again, the overridden color remains the + // same for both the original object and the inherited object. + item.Kirigami.Theme.colorSet = Kirigami.Theme.View + + compare(item.color, "#ff0000") + compare(item.child1.color, "#ff0000") + compare(item.child2.color, "#eff0f1") + + // If we override a color of the item that inherits, it will stay the + // same even if that color changes in the parent. + item.child1.Kirigami.Theme.backgroundColor = "#00ff00" + item.Kirigami.Theme.backgroundColor = "#0000ff" + + compare(item.color, "#0000ff") + compare(item.child1.color, "#00ff00") + compare(item.child2.color, "#eff0f1") + } + + Component { + id: deepInherit + + Rectangle { + // Ensure the root object we test with doesn't accidentally inherit + // from the parent test object. + Kirigami.Theme.inherit: false + + color: Kirigami.Theme.backgroundColor + + property alias child1: rect1 + property alias child2: rect2 + property alias child3: rect3 + + Rectangle { + id: rect1 + color: Kirigami.Theme.backgroundColor + + Rectangle { + id: rect2 + color: Kirigami.Theme.backgroundColor + + Rectangle { + id: rect3 + color: Kirigami.Theme.backgroundColor + } + } + } + } + } + + function test_inherit_deep() { + var item = createTemporaryObject(deepInherit, testCase) + verify(item) + + compare(item.color, "#eff0f1") + compare(item.child1.color, "#eff0f1") + compare(item.child2.color, "#eff0f1") + compare(item.child3.color, "#eff0f1") + + item.Kirigami.Theme.backgroundColor = "#ff0000" + + compare(item.color, "#ff0000") + compare(item.child1.color, "#ff0000") + compare(item.child2.color, "#ff0000") + compare(item.child3.color, "#ff0000") + + item.child2.Kirigami.Theme.inherit = false + item.child2.Kirigami.Theme.backgroundColor = "#00ff00" + + compare(item.color, "#ff0000") + compare(item.child1.color, "#ff0000") + compare(item.child2.color, "#00ff00") + compare(item.child3.color, "#00ff00") + + item.child2.Kirigami.Theme.inherit = true + item.child2.Kirigami.Theme.backgroundColor = undefined + + compare(item.color, "#ff0000") + compare(item.child1.color, "#ff0000") + compare(item.child2.color, "#ff0000") + compare(item.child3.color, "#ff0000") + + item.child2.Kirigami.Theme.colorSet = Kirigami.Theme.Complementary + item.child2.Kirigami.Theme.inherit = false + + compare(item.color, "#ff0000") + compare(item.child1.color, "#ff0000") + compare(item.child2.color, "#31363b") + compare(item.child3.color, "#31363b") + } + + Component { + id: colorSet + + Rectangle { + // Ensure the root object we test with doesn't accidentally inherit + // from the parent test object. + Kirigami.Theme.inherit: false + + Kirigami.Theme.colorSet: Kirigami.Theme.View + color: Kirigami.Theme.backgroundColor + } + } + + function test_colorset() { + var item = createTemporaryObject(colorSet, testCase) + verify(item) + + compare(item.color, "#fcfcfc") + + item.Kirigami.Theme.colorSet = Kirigami.Theme.Complementary + + compare(item.color, "#31363b") + } + + Component { + id: colorGroup + + Rectangle { + // Ensure the root object we test with doesn't accidentally inherit + // from the parent test object. + Kirigami.Theme.inherit: false + + Kirigami.Theme.colorGroup: Kirigami.Theme.Disabled + color: Kirigami.Theme.backgroundColor + } + } + + function test_colorGroup() { + var item = createTemporaryObject(colorGroup, testCase) + verify(item) + + var color = Qt.tint("#eff0f1", "transparent") + + compare(item.color, Qt.hsva(color.hsvHue, color.hsvSaturation * 0.5, color.hsvValue * 0.8)) + + item.Kirigami.Theme.colorGroup = Kirigami.Theme.Inactive + + compare(item.color, Qt.hsva(color.hsvHue, color.hsvSaturation * 0.5, color.hsvValue)) + } + + Component { + id: palette + + Rectangle { + color: Kirigami.Theme.backgroundColor + + property alias child: button + + Button { + id: button + palette: Kirigami.Theme.palette + } + } + } + + function test_palette() { + skip("palette property type has changed in Qt 6, Kirigami.Theme and this test case need adjustments") + var item = createTemporaryObject(palette, testCase) + verify(item) + + compare(item.child.background.color, "#eff0f1") + compare(item.child.contentItem.color, "#31363b") + + item.Kirigami.Theme.backgroundColor = "#ff0000" + + compare(item.child.background.color, "#ff0000") + } + + Component { + id: signalCount + + Rectangle { + id: rect + + Kirigami.Theme.inherit: false + + color: Kirigami.Theme.backgroundColor + + property SignalSpy signalSpy: SignalSpy { + target: rect.Kirigami.Theme + signalName: "colorsChanged" + } + } + } + + function test_signal_count() { + var item = createTemporaryObject(signalCount, testCase) + verify(item) + verify(item.signalSpy.valid) + compare(item.signalSpy.count, 0) + + item.Kirigami.Theme.colorSet = Kirigami.Theme.View + compare(item.signalSpy.count, 1) + + item.Kirigami.Theme.colorSet = Kirigami.Theme.Window + compare(item.signalSpy.count, 2) + } + + Component { + id: disable + + Rectangle { + // Ensure the root object we test with doesn't accidentally inherit + // from the parent test object. + Kirigami.Theme.inherit: false + + color: Kirigami.Theme.textColor + + property alias child1: rect1 + property alias child2: rect2 + + Rectangle { + id: rect1 + color: Kirigami.Theme.textColor + Rectangle { + id: rect2 + color: Kirigami.Theme.textColor + } + } + } + } + + function test_disable() { + var item = createTemporaryObject(disable, testCase) + verify(item) + verify(waitForRendering(item)) + + // Default values are all the same + compare(item.color, "#31363b") + compare(item.child1.color, "#31363b") + compare(item.child2.color, "#31363b") + + item.enabled = false + verify(waitForRendering(item)) + + compare(item.color, "#2b2d2f") + compare(item.child1.color, "#2b2d2f") + compare(item.child2.color, "#2b2d2f") + + item.enabled = true + item.child1.enabled = false + verify(waitForRendering(item)) + + compare(item.color, "#31363b") + compare(item.child1.color, "#2b2d2f") + compare(item.child2.color, "#2b2d2f") + } +} diff --git a/autotests/wheelhandler/ContentFlickable.qml b/autotests/wheelhandler/ContentFlickable.qml new file mode 100644 index 0000000..fe11439 --- /dev/null +++ b/autotests/wheelhandler/ContentFlickable.qml @@ -0,0 +1,65 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T + +Flickable { + id: flickable + property real cellWidth: 60 + property real cellHeight: 60 + readonly property T.Button enableSliderButton: enableSliderButton + readonly property T.Slider slider: slider + implicitWidth: cellWidth * 10 + leftMargin + rightMargin + implicitHeight: cellHeight * 10 + topMargin + bottomMargin + contentWidth: contentItem.childrenRect.width + contentHeight: contentItem.childrenRect.height + Grid { + id: grid + columns: Math.sqrt(visibleChildren.length) + Repeater { + model: 500 + delegate: Rectangle { + implicitWidth: flickable.cellWidth + implicitHeight: flickable.cellHeight + gradient: Gradient { + orientation: index % 2 ? Gradient.Vertical : Gradient.Horizontal + GradientStop { position: 0; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) } + GradientStop { position: 1; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) } + } + } + } + QQC2.Button { + id: enableSliderButton + width: flickable.cellWidth + height: flickable.cellHeight + contentItem: QQC2.Label { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: "Enable Slider" + wrapMode: Text.Wrap + } + checked: true + } + QQC2.Slider { + id: slider + enabled: enableSliderButton.checked + width: flickable.cellWidth + height: flickable.cellHeight + } + Repeater { + model: 500 + delegate: Rectangle { + implicitWidth: flickable.cellWidth + implicitHeight: flickable.cellHeight + gradient: Gradient { + orientation: index % 2 ? Gradient.Vertical : Gradient.Horizontal + GradientStop { position: 0; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) } + GradientStop { position: 1; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) } + } + } + } + } +} diff --git a/autotests/wheelhandler/ScrollableFlickable.qml b/autotests/wheelhandler/ScrollableFlickable.qml new file mode 100644 index 0000000..c8ac2b4 --- /dev/null +++ b/autotests/wheelhandler/ScrollableFlickable.qml @@ -0,0 +1,30 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +ContentFlickable { + id: flickable + leftMargin: QQC2.ScrollBar.vertical && QQC2.ScrollBar.vertical.visible && QQC2.ScrollBar.vertical.mirrored ? QQC2.ScrollBar.vertical.width : 0 + rightMargin: QQC2.ScrollBar.vertical && QQC2.ScrollBar.vertical.visible && !QQC2.ScrollBar.vertical.mirrored ? QQC2.ScrollBar.vertical.width : 0 + bottomMargin: QQC2.ScrollBar.horizontal && QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.height : 0 + QQC2.ScrollBar.vertical: QQC2.ScrollBar { + parent: flickable.parent + anchors.right: flickable.right + anchors.top: flickable.top + anchors.bottom: flickable.bottom + anchors.bottomMargin: flickable.QQC2.ScrollBar.horizontal ? flickable.QQC2.ScrollBar.horizontal.height : anchors.margins + active: flickable.QQC2.ScrollBar.vertical.active + } + QQC2.ScrollBar.horizontal: QQC2.ScrollBar { + parent: flickable.parent + anchors.left: flickable.left + anchors.right: flickable.right + anchors.bottom: flickable.bottom + anchors.rightMargin: flickable.QQC2.ScrollBar.vertical ? flickable.QQC2.ScrollBar.vertical.width : anchors.margins + active: flickable.QQC2.ScrollBar.horizontal.active + } +} diff --git a/autotests/wheelhandler/tst_filterMouseEvents.qml b/autotests/wheelhandler/tst_filterMouseEvents.qml new file mode 100644 index 0000000..7867bfb --- /dev/null +++ b/autotests/wheelhandler/tst_filterMouseEvents.qml @@ -0,0 +1,75 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import QtTest + +TestCase { + id: root + name: "WheelHandler filterMouseEvents" + visible: true + when: windowShown + width: flickable.implicitWidth + height: flickable.implicitHeight + + function test_MouseFlick() { + const x = flickable.contentX + const y = flickable.contentY + mousePress(flickable, flickable.leftMargin + 10, 10) + mouseMove(flickable) + mouseRelease(flickable) + verify(flickable.contentX === x && flickable.contentY === y, "not moved") + } + + // NOTE: Unfortunately, this test can't work. Flickable does not handle touch events, only mouse events synthesized from touch events + // TODO: Uncomment if Flickable is ever able to use touch events. + /*function test_TouchFlick() { + const x = flickable.contentX, y = flickable.contentY + let touch = touchEvent(flickable) + // Press on center. + touch.press(0, flickable) + touch.commit() + // Move a bit towards top left. + touch.move(0, flickable, flickable.width/2 - 50, flickable.height/2 - 50) + touch.commit() + // Release at the spot we moved to. + touch.release(0, flickable, flickable.width/2 - 50, flickable.height/2 - 50) + touch.commit() + verify(flickable.contentX !== x || flickable.contentY !== y, "moved") + }*/ + + function test_MouseScrollBars() { + const x = flickable.contentX, y = flickable.contentY + mousePress(flickable, flickable.leftMargin + 10, 10) + mouseMove(flickable) + const interactive = flickable.QQC2.ScrollBar.vertical.interactive || flickable.QQC2.ScrollBar.horizontal.interactive + mouseRelease(flickable) + verify(interactive, "interactive scrollbars") + } + + function test_TouchScrollBars() { + const x = flickable.contentX, y = flickable.contentY + let touch = touchEvent(flickable) + touch.press(0, flickable, flickable.leftMargin + 10, 10) + touch.commit() + touch.move(0, flickable) + touch.commit() + const interactive = flickable.QQC2.ScrollBar.vertical.interactive || flickable.QQC2.ScrollBar.horizontal.interactive + touch.release(0, flickable) + touch.commit() + verify(!interactive, "no interactive scrollbars") + } + + ScrollableFlickable { + id: flickable + anchors.fill: parent + Kirigami.WheelHandler { + id: wheelHandler + target: flickable + filterMouseEvents: true + } + } +} diff --git a/autotests/wheelhandler/tst_invokables.qml b/autotests/wheelhandler/tst_invokables.qml new file mode 100644 index 0000000..a4ebd03 --- /dev/null +++ b/autotests/wheelhandler/tst_invokables.qml @@ -0,0 +1,75 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import QtTest + +TestCase { + id: root + readonly property real hstep: wheelHandler.horizontalStepSize + readonly property real vstep: wheelHandler.verticalStepSize + readonly property real pageWidth: flickable.width - flickable.leftMargin - flickable.rightMargin + readonly property real pageHeight: flickable.height - flickable.topMargin - flickable.bottomMargin + readonly property real contentWidth: flickable.contentWidth + readonly property real contentHeight: flickable.contentHeight + property alias wheelHandler: wheelHandler + property alias flickable: flickable + + name: "WheelHandler invokable functions" + visible: true + when: windowShown + width: flickable.implicitWidth + height: flickable.implicitHeight + + function test_Invokables() { + const originalX = flickable.contentX + const originalY = flickable.contentY + let x = originalX + let y = originalY + + wheelHandler.scrollRight() + tryCompare(flickable, "contentX", x + hstep, Kirigami.Units.longDuration * 2, "scrollRight()") + x = flickable.contentX + + wheelHandler.scrollLeft() + tryCompare(flickable, "contentX", x - hstep, Kirigami.Units.longDuration * 2, "scrollLeft()") + x = flickable.contentX + + wheelHandler.scrollDown() + tryCompare(flickable, "contentY", y + vstep, Kirigami.Units.longDuration * 2, "scrollDown()") + y = flickable.contentY + + wheelHandler.scrollUp() + tryCompare(flickable, "contentY", y - vstep, Kirigami.Units.longDuration * 2, "scrollUp()") + y = flickable.contentY + + wheelHandler.scrollRight(101) + tryCompare(flickable, "contentX", x + 101, Kirigami.Units.longDuration * 2, "scrollRight(101)") + x = flickable.contentX + + wheelHandler.scrollLeft(101) + tryCompare(flickable, "contentX", x - 101, Kirigami.Units.longDuration * 2, "scrollLeft(101)") + x = flickable.contentX + + wheelHandler.scrollDown(101) + tryCompare(flickable, "contentY", y + 101, Kirigami.Units.longDuration * 2, "scrollDown(101)") + y = flickable.contentY + + wheelHandler.scrollUp(101) + tryCompare(flickable, "contentY", y - 101, Kirigami.Units.longDuration * 2, "scrollUp(101)") + y = flickable.contentY + } + + ScrollableFlickable { + id: flickable + anchors.fill: parent + Kirigami.WheelHandler { + id: wheelHandler + target: flickable + } + } +} + diff --git a/autotests/wheelhandler/tst_onWheel.qml b/autotests/wheelhandler/tst_onWheel.qml new file mode 100644 index 0000000..7880474 --- /dev/null +++ b/autotests/wheelhandler/tst_onWheel.qml @@ -0,0 +1,107 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import QtTest + +TestCase { + id: root + name: "WheelHandler onWheel" + visible: true + when: windowShown + width: 600 + height: 600 + + function test_onWheel() { + let contentX = flickable.contentX + let contentY = flickable.contentY + let contentWidth = flickable.contentWidth + let contentHeight = flickable.contentHeight + + // grow + mouseWheel(flickable, flickable.leftMargin, 0, -120, -120, Qt.NoButton, Qt.ControlModifier) + verify(flickable.contentWidth === contentWidth - 120, "-xDelta") + contentWidth = flickable.contentWidth + verify(flickable.contentHeight === contentHeight - 120, "-yDelta") + contentHeight = flickable.contentHeight + + // check if accepting the event prevents scrolling + verify(flickable.contentX === contentX && flickable.contentY === contentY, "not moved") + + // shrink + mouseWheel(flickable, flickable.leftMargin, 0, 120, 120, Qt.NoButton, Qt.ControlModifier) + verify(flickable.contentWidth === contentWidth + 120, "+xDelta") + verify(flickable.contentHeight === contentHeight + 120, "+yDelta") + + // check if accepting the event prevents scrolling + verify(flickable.contentX === contentX && flickable.contentY === contentY, "not moved") + } + + Rectangle { + anchors.fill: parent + color: "black" + } + + Flickable { + id: flickable + anchors.fill: parent + Kirigami.WheelHandler { + id: wheelHandler + target: flickable + onWheel: { + if (wheel.modifiers & Qt.ControlModifier) { + // Adding delta is the simplest way to change size without running into floating point number issues + // NOTE: Not limiting minimum content size to a size greater than the Flickable size makes it so + // wheel events stop coming to onWheel when the content size is the size of the flickable or smaller. + // Maybe it's a Flickable issue? Koko had the same problem with Flickable. + flickable.contentWidth = Math.max(720, flickable.contentWidth + wheel.angleDelta.x) + flickable.contentHeight = Math.max(720, flickable.contentHeight + wheel.angleDelta.y) + wheel.accepted = true + } + } + } + leftMargin: QQC2.ScrollBar.vertical && QQC2.ScrollBar.vertical.visible && QQC2.ScrollBar.vertical.mirrored ? QQC2.ScrollBar.vertical.width : 0 + rightMargin: QQC2.ScrollBar.vertical && QQC2.ScrollBar.vertical.visible && !QQC2.ScrollBar.vertical.mirrored ? QQC2.ScrollBar.vertical.width : 0 + bottomMargin: QQC2.ScrollBar.horizontal && QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.height : 0 + QQC2.ScrollBar.vertical: QQC2.ScrollBar { + parent: flickable.parent + anchors.right: flickable.right + anchors.top: flickable.top + anchors.bottom: flickable.bottom + anchors.bottomMargin: flickable.QQC2.ScrollBar.horizontal ? flickable.QQC2.ScrollBar.horizontal.height : anchors.margins + active: flickable.QQC2.ScrollBar.vertical.active + } + QQC2.ScrollBar.horizontal: QQC2.ScrollBar { + parent: flickable.parent + anchors.left: flickable.left + anchors.right: flickable.right + anchors.bottom: flickable.bottom + anchors.rightMargin: flickable.QQC2.ScrollBar.vertical ? flickable.QQC2.ScrollBar.vertical.width : anchors.margins + active: flickable.QQC2.ScrollBar.horizontal.active + } + contentWidth: 1200 + contentHeight: 1200 + Rectangle { + id: contentRect + anchors.fill: parent + gradient: Gradient.WideMatrix + border.color: Qt.rgba(0,0,0,0.5) + border.width: 2 + } + } + + QQC2.Label { + anchors.centerIn: parent + leftPadding: 4 + rightPadding: 4 + wrapMode: Text.Wrap + color: "white" + text: `Rectangle size: ${contentRect.width}x${contentRect.height}` + background: Rectangle { + color: "black" + } + } +} diff --git a/autotests/wheelhandler/tst_scrolling.qml b/autotests/wheelhandler/tst_scrolling.qml new file mode 100644 index 0000000..425dcc9 --- /dev/null +++ b/autotests/wheelhandler/tst_scrolling.qml @@ -0,0 +1,212 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import QtTest + +TestCase { + id: root + readonly property real hstep: wheelHandler.horizontalStepSize + readonly property real vstep: wheelHandler.verticalStepSize + readonly property real pageWidth: flickable.width - flickable.leftMargin - flickable.rightMargin + readonly property real pageHeight: flickable.height - flickable.topMargin - flickable.bottomMargin + readonly property real contentWidth: flickable.contentWidth + readonly property real contentHeight: flickable.contentHeight + property alias wheelHandler: wheelHandler + property alias flickable: flickable + + name: "WheelHandler scrolling" + visible: true + when: windowShown + width: flickable.implicitWidth + height: flickable.implicitHeight + + function wheelScrolling(angleDelta = wheelHandler.verticalStepSize) { + let x = flickable.contentX + let y = flickable.contentY + const angleDeltaFactor = angleDelta / 120 + mouseWheel(flickable, flickable.leftMargin, 0, -angleDelta, -angleDelta, Qt.NoButton) + if (angleDelta === wheelHandler.verticalStepSize) { + tryCompare(flickable, "contentX", Math.round(x + hstep * angleDeltaFactor), Kirigami.Units.longDuration * 2, "+xTick") + } else { + compare(flickable.contentX, Math.round(x + hstep * angleDeltaFactor), "+xTick") + } + x = flickable.contentX + if (angleDelta === wheelHandler.verticalStepSize) { + tryCompare(flickable, "contentY", Math.round(y + vstep * angleDeltaFactor), Kirigami.Units.longDuration * 2, "+yTick") + } else { + compare(flickable.contentY, Math.round(y + vstep * angleDeltaFactor), "+yTick") + } + y = flickable.contentY + + mouseWheel(flickable, flickable.leftMargin, 0, angleDelta, angleDelta, Qt.NoButton) + if (angleDelta === wheelHandler.verticalStepSize) { + tryCompare(flickable, "contentX", Math.round(x - hstep * angleDeltaFactor), Kirigami.Units.longDuration * 2, "-xTick") + } else { + compare(flickable.contentX, Math.round(x - hstep * angleDeltaFactor), "-xTick") + } + x = flickable.contentX + if (angleDelta === wheelHandler.verticalStepSize) { + tryCompare(flickable, "contentY", Math.round(y - vstep * angleDeltaFactor), Kirigami.Units.longDuration * 2, "-yTick") + } else { + compare(flickable.contentY, Math.round(y - vstep * angleDeltaFactor), "-yTick") + } + y = flickable.contentY + + if (Qt.platform.pluginName !== "xcb") { + mouseWheel(flickable, flickable.leftMargin, 0, 0, -angleDelta, Qt.NoButton, Qt.AltModifier) + tryCompare(flickable, "contentX", Math.round(x + hstep * angleDeltaFactor), Kirigami.Units.longDuration * 2, "+h_yTick") + x = flickable.contentX + tryCompare(flickable, "contentY", y, Kirigami.Units.longDuration * 2, "no +yTick") + + mouseWheel(flickable, flickable.leftMargin, 0, 0, angleDelta, Qt.NoButton, Qt.AltModifier) + tryCompare(flickable, "contentX", Math.round(x - hstep * angleDeltaFactor), Kirigami.Units.longDuration * 2, "-h_yTick") + x = flickable.contentX + tryCompare(flickable, "contentY", y, Kirigami.Units.longDuration * 2, "no -yTick") + } + + mouseWheel(flickable, flickable.leftMargin, 0, -angleDelta, -angleDelta, Qt.NoButton, wheelHandler.pageScrollModifiers) + if (angleDelta === wheelHandler.verticalStepSize) { + tryCompare(flickable, "contentX", Math.round(x + pageWidth * angleDeltaFactor), Kirigami.Units.longDuration * 2, "+xPage") + } else { + compare(flickable.contentX, Math.round(x + pageWidth * angleDeltaFactor), "+xPage") + } + x = flickable.contentX + if (angleDelta === wheelHandler.verticalStepSize) { + tryCompare(flickable, "contentY", Math.round(y + pageHeight * angleDeltaFactor), Kirigami.Units.longDuration * 2, "+yPage") + } else { + compare(flickable.contentY, Math.round(y + pageHeight * angleDeltaFactor), "+yPage") + } + y = flickable.contentY + + mouseWheel(flickable, flickable.leftMargin, 0, angleDelta, angleDelta, Qt.NoButton, wheelHandler.pageScrollModifiers) + if (angleDelta === wheelHandler.verticalStepSize) { + tryCompare(flickable, "contentX", Math.round(x - pageWidth * angleDeltaFactor), Kirigami.Units.longDuration * 2, "-xPage") + } else { + compare(flickable.contentX, Math.round(x - pageWidth * angleDeltaFactor), "-xPage") + } + x = flickable.contentX + if (angleDelta === wheelHandler.verticalStepSize) { + tryCompare(flickable, "contentY", Math.round(y - pageHeight * angleDeltaFactor), Kirigami.Units.longDuration * 2, "-yPage") + } else { + compare(flickable.contentY, Math.round(y - pageHeight * angleDeltaFactor), "-yPage") + } + y = flickable.contentY + + if (Qt.platform.pluginName !== "xcb") { + mouseWheel(flickable, flickable.leftMargin, 0, 0, -angleDelta, Qt.NoButton, + Qt.AltModifier | wheelHandler.pageScrollModifiers) + tryCompare(flickable, "contentX", Math.round(x + pageWidth * angleDeltaFactor), Kirigami.Units.longDuration * 2, "+h_yPage") + x = flickable.contentX + tryCompare(flickable, "contentY", y, Kirigami.Units.longDuration * 2, "no +yPage") + + mouseWheel(flickable, flickable.leftMargin, 0, 0, angleDelta, Qt.NoButton, + Qt.AltModifier | wheelHandler.pageScrollModifiers) + tryCompare(flickable, "contentX", Math.round(x - pageWidth * angleDeltaFactor), Kirigami.Units.longDuration * 2, "-h_yPage") + x = flickable.contentX + tryCompare(flickable, "contentY", y, Kirigami.Units.longDuration * 2, "no -yPage") + } + } + + function test_WheelScrolling() { + // HID 1bcf:08a0 Mouse + // Angle delta is 120, like most mice. + wheelScrolling() + } + + function test_HiResWheelScrolling() { + // Logitech MX Master 3 + // Main wheel angle delta is at least 16, plus multiples of 8 when scrolling faster. + wheelScrolling(16) + } + + function test_TouchpadScrolling() { + // UNIW0001:00 093A:0255 Touchpad + // 2 finger scroll angle delta is at least 3, but larger increments are used when scrolling faster. + wheelScrolling(3) + } + + function keyboardScrolling() { + const originalX = flickable.contentX + const originalY = flickable.contentY + let x = originalX + let y = originalY + keyClick(Qt.Key_Right) + tryCompare(flickable, "contentX", x + hstep, Kirigami.Units.longDuration * 2, "Key_Right") + x = flickable.contentX + + keyClick(Qt.Key_Left) + tryCompare(flickable, "contentX", x - hstep, Kirigami.Units.longDuration * 2, "Key_Left") + x = flickable.contentX + + keyClick(Qt.Key_Down) + tryCompare(flickable, "contentY", y + vstep, Kirigami.Units.longDuration * 2, "Key_Down") + y = flickable.contentY + + keyClick(Qt.Key_Up) + tryCompare(flickable, "contentY", y - vstep, Kirigami.Units.longDuration * 2, "Key_Up") + y = flickable.contentY + + keyClick(Qt.Key_PageDown) + tryCompare(flickable, "contentY", y + pageHeight, Kirigami.Units.longDuration * 2, "Key_PageDown") + y = flickable.contentY + + keyClick(Qt.Key_PageUp) + tryCompare(flickable, "contentY", y - pageHeight, Kirigami.Units.longDuration * 2, "Key_PageUp") + y = flickable.contentY + + keyClick(Qt.Key_End) + tryCompare(flickable, "contentY", contentHeight - pageHeight, Kirigami.Units.longDuration * 2, "Key_End") + y = flickable.contentY + + keyClick(Qt.Key_Home) + tryCompare(flickable, "contentY", originalY, Kirigami.Units.longDuration * 2, "Key_Home") + y = flickable.contentY + + keyClick(Qt.Key_PageDown, Qt.AltModifier) + tryCompare(flickable, "contentX", x + pageWidth, Kirigami.Units.longDuration * 2, "h_Key_PageDown") + x = flickable.contentX + + keyClick(Qt.Key_PageUp, Qt.AltModifier) + tryCompare(flickable, "contentX", x - pageWidth, Kirigami.Units.longDuration * 2, "h_Key_PageUp") + x = flickable.contentX + + keyClick(Qt.Key_End, Qt.AltModifier) + tryCompare(flickable, "contentX", contentWidth - pageWidth, Kirigami.Units.longDuration * 2, "h_Key_End") + x = flickable.contentX + + keyClick(Qt.Key_Home, Qt.AltModifier) + tryCompare(flickable, "contentX", originalX, Kirigami.Units.longDuration * 2, "h_Key_Home") + } + + function test_KeyboardScrolling() { + keyboardScrolling() + } + + function test_StepSize() { + // 101 is a value unlikely to be used by any user's combination of settings and hardware + wheelHandler.verticalStepSize = 101 + wheelHandler.horizontalStepSize = 101 + wheelScrolling() + keyboardScrolling() + // reset to default + wheelHandler.verticalStepSize = undefined + wheelHandler.horizontalStepSize = undefined + verify(wheelHandler.verticalStepSize == 20 * Qt.styleHints.wheelScrollLines, "default verticalStepSize") + verify(wheelHandler.horizontalStepSize == 20 * Qt.styleHints.wheelScrollLines, "default horizontalStepSize") + } + + ScrollableFlickable { + id: flickable + focus: true + anchors.fill: parent + Kirigami.WheelHandler { + id: wheelHandler + target: flickable + keyNavigationEnabled: true + } + } +} diff --git a/config-OpenMP.h.cmake b/config-OpenMP.h.cmake new file mode 100644 index 0000000..76f6d25 --- /dev/null +++ b/config-OpenMP.h.cmake @@ -0,0 +1 @@ +#cmakedefine01 HAVE_OpenMP diff --git a/docs/pics/BasicListItemReserve.svg b/docs/pics/BasicListItemReserve.svg new file mode 100644 index 0000000..dac57c4 --- /dev/null +++ b/docs/pics/BasicListItemReserve.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/pics/BasicListItemTypes.svg b/docs/pics/BasicListItemTypes.svg new file mode 100644 index 0000000..3d38b1d --- /dev/null +++ b/docs/pics/BasicListItemTypes.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/pics/ContextualHelpButton.png b/docs/pics/ContextualHelpButton.png new file mode 100644 index 0000000..15a340c Binary files /dev/null and b/docs/pics/ContextualHelpButton.png differ diff --git a/docs/pics/MinimalExample.webp b/docs/pics/MinimalExample.webp new file mode 100644 index 0000000..0b0ca2e Binary files /dev/null and b/docs/pics/MinimalExample.webp differ diff --git a/docs/pics/PageRouterModel.svg b/docs/pics/PageRouterModel.svg new file mode 100644 index 0000000..cedea9e --- /dev/null +++ b/docs/pics/PageRouterModel.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/docs/pics/PageRouterNavigate.svg b/docs/pics/PageRouterNavigate.svg new file mode 100644 index 0000000..e128ba3 --- /dev/null +++ b/docs/pics/PageRouterNavigate.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/docs/pics/PageRouterPop.svg b/docs/pics/PageRouterPop.svg new file mode 100644 index 0000000..ee9bb4d --- /dev/null +++ b/docs/pics/PageRouterPop.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/docs/pics/PageRouterPush.svg b/docs/pics/PageRouterPush.svg new file mode 100644 index 0000000..a74a1e9 --- /dev/null +++ b/docs/pics/PageRouterPush.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/docs/pics/icon/active.png b/docs/pics/icon/active.png new file mode 100644 index 0000000..476efc8 Binary files /dev/null and b/docs/pics/icon/active.png differ diff --git a/docs/pics/icon/selected.png b/docs/pics/icon/selected.png new file mode 100644 index 0000000..d5beebf Binary files /dev/null and b/docs/pics/icon/selected.png differ diff --git a/docs/pics/searchdialog.png b/docs/pics/searchdialog.png new file mode 100644 index 0000000..3240d86 Binary files /dev/null and b/docs/pics/searchdialog.png differ diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..e43feb4 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,2 @@ + +add_subdirectory(applicationitemapp) diff --git a/examples/Image Colors/IconPage.qml b/examples/Image Colors/IconPage.qml new file mode 100644 index 0000000..23072c2 --- /dev/null +++ b/examples/Image Colors/IconPage.qml @@ -0,0 +1,179 @@ +/* + * SPDX-FileCopyrightText: 2024 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +import "components" as Components + +Kirigami.ScrollablePage { + id: root + + readonly property list __icons: [ + "desktop", + "firefox", + "vlc", + "blender", + "applications-games", + "blinken", + "adjustlevels", + "adjustrgb", + "cuttlefish", + "folder-games", + "applications-network", + "multimedia-player", + "applications-utilities", + "accessories-dictionary", + "calligraflow", + "calligrakrita", + "view-left-close", + "calligraauthor", + ] + + property int __currentIconIndex: 0 + + function currentIcon(): string { + return __icons[__currentIconIndex]; + } + + function previousIcon(): void { + __currentIconIndex = (__currentIconIndex + __icons.length - 1) % __icons.length; + } + + function nextIcon(): void { + __currentIconIndex = (__currentIconIndex + 1) % __icons.length; + } + + Kirigami.ImageColors { + id: iconPalette + source: icon.source + } + + implicitWidth: Kirigami.Units.gridUnit * 20 + + header: QQC2.ToolBar { + contentItem: Kirigami.ActionToolBar { + alignment: Qt.AlignHCenter + actions: [ + Kirigami.Action { + text: "Previous" + icon.name: "go-previous-symbolic" + displayHint: Kirigami.DisplayHint.KeepVisible + onTriggered: source => { + root.previousIcon(); + } + }, + Kirigami.Action { + text: "Next" + icon.name: "go-next-symbolic" + displayHint: Kirigami.DisplayHint.KeepVisible + onTriggered: source => { + root.nextIcon(); + } + } + ] + } + } + + ColumnLayout { + spacing: Kirigami.Units.gridUnit + + Components.Section { + title: "Icon Name" + + Kirigami.SelectableLabel { + text: currentIcon() + horizontalAlignment: TextEdit.AlignHCenter + Layout.fillWidth: true + } + } + + Components.Section { + title: "Icon" + + QQC2.Label { + text: "Background" + } + + QQC2.ButtonGroup { + id: backgroundGroup + Component.onCompleted: { + checkedButton = transparentRadioButton; + } + } + + QQC2.RadioButton { + id: transparentRadioButton + text: "Transparent" + Layout.fillWidth: true + QQC2.ButtonGroup.group: backgroundGroup + } + QQC2.RadioButton { + id: kirigamiThemeRadioButton + text: "Kirigami.Theme.backgroundColor" + Layout.fillWidth: true + QQC2.ButtonGroup.group: backgroundGroup + } + QQC2.RadioButton { + id: contrastRadioButton + text: "Contrast Color" + Layout.fillWidth: true + QQC2.ButtonGroup.group: backgroundGroup + } + + Components.ColorWell { + Layout.alignment: Qt.AlignHCenter + Layout.minimumWidth: 100 + Layout.preferredWidth: 200 + Layout.preferredHeight: 200 + Layout.maximumWidth: 200 + Layout.fillWidth: true + + color: { + switch (backgroundGroup.checkedButton) { + case transparentRadioButton: + default: + return "transparent"; + case kirigamiThemeRadioButton: + return Kirigami.Theme.backgroundColor; + case contrastRadioButton: + return iconPalette.dominantContrast; + } + } + showLabel: false + + Kirigami.Icon { + id: icon + anchors.centerIn: parent + width: Math.min(128, parent.width - Kirigami.Units.largeSpacing) + height: width + source: root.currentIcon() + } + } + } + + Components.Section { + title: "Average Color" + + Components.ColorWell { + Layout.fillWidth: true + color: iconPalette.average + } + } + + Components.Section { + title: "Icon Palette" + + Components.PaletteTable { + swatches: iconPalette.palette + } + } + } +} diff --git a/examples/Image Colors/ImagePage.qml b/examples/Image Colors/ImagePage.qml new file mode 100644 index 0000000..58a174e --- /dev/null +++ b/examples/Image Colors/ImagePage.qml @@ -0,0 +1,189 @@ +/* + * SPDX-FileCopyrightText: 2024 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Dialogs as QtDialogs +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +import "components" as Components +import "ImageProviders.mjs" as ImageProviders + +QQC2.Page { + id: root + + readonly property alias image: image + + readonly property Kirigami.ImageColors imagePalette: Kirigami.ImageColors { + id: imagePalette + source: image + } + + readonly property /*ImageProviders.ImageData*/ var imageData: __imageData + + readonly property Item overlay: overlay + + property /*XMLHttpRequest*/var __xhr: null + property /*ImageProviders.ImageData*/ var __imageData: null + + function __abort(): void { + if (__xhr !== null) { + __xhr.abort(); + __xhr = null; + } + } + + function randomizeSource(): void { + __abort(); + const provider = new ImageProviders.UnsplashProvider(); + __xhr = provider.getRandom(imageData => { + __xhr = null; + __imageData = imageData; + }); + } + + function openChooserDialog(): void { + fileDialog.open(); + } + + function setImageSource(fileUrl: url): void { + __abort(); + const provider = new ImageProviders.FileProvider(fileUrl); + __imageData = provider.imageData(); + } + + Component.onCompleted: { + randomizeSource(); + } + + header: QQC2.ToolBar { + contentItem: Kirigami.ActionToolBar { + alignment: Qt.AlignHCenter + actions: [ + Kirigami.Action { + icon.name: "shuffle-symbolic" + text: "Random Image" + displayHint: Kirigami.DisplayHint.KeepVisible + onTriggered: source => { + root.randomizeSource(); + } + }, + Kirigami.Action { + icon.name: "insert-image-symbolic" + text: "Open Image…" + displayHint: Kirigami.DisplayHint.KeepVisible + onTriggered: source => { + root.openChooserDialog(); + } + } + ] + } + } + + QtDialogs.FileDialog { + id: fileDialog + + title: "Open Image" + nameFilters: "Media Files (*.jpg *.png *.svg)" + fileMode: QtDialogs.FileDialog.OpenFile + options: QtDialogs.FileDialog.ReadOnly + + onAccepted: { + root.setImageSource(selectedFile); + } + } + + Image { + id: image + + anchors.fill: parent + source: root.__imageData?.image_url ?? ""; + fillMode: Image.PreserveAspectFit + + onStatusChanged: { + imagePalette.update(); + } + } + + QQC2.Pane { + id: overlay + + anchors.centerIn: parent + + width: Kirigami.Units.gridUnit * 15 + + Kirigami.Theme.backgroundColor: imagePalette.background + Kirigami.Theme.textColor: imagePalette.foreground + Kirigami.Theme.highlightColor: imagePalette.highlight + + background: Rectangle { + color: Kirigami.Theme.backgroundColor + border.width: 1 + border.color: Kirigami.Theme.textColor + radius: Kirigami.Units.cornerRadius + opacity: 0.8 + } + + contentItem: ColumnLayout { + spacing: Kirigami.Units.largeSpacing + + QQC2.Label { + text: "Highlight Color:\nLorem Ipsum dolor sit amet" + color: Kirigami.Theme.highlightColor + wrapMode: Text.Wrap + } + + Components.ColorWell { + Layout.fillWidth: true + color: Kirigami.Theme.highlightColor + } + + Kirigami.Separator { + Layout.fillWidth: true + } + + QQC2.Label { + text: "Text Color:\nLorem Ipsum dolor sit amet" + color: Kirigami.Theme.textColor + wrapMode: Text.Wrap + } + + Components.ColorWell { + Layout.fillWidth: true + color: Kirigami.Theme.textColor + } + + Kirigami.Separator { + Layout.fillWidth: true + } + + QQC2.Label { + text: "Controls with inherited theme" + color: Kirigami.Theme.textColor + wrapMode: Text.Wrap + } + + + RowLayout { + spacing: Kirigami.Units.largeSpacing + + QQC2.TextField { + Kirigami.Theme.inherit: true + Layout.fillWidth: true + text: "text" + } + + QQC2.Button { + Kirigami.Theme.inherit: true + text: "Ok" + } + } + } + } +} diff --git a/examples/Image Colors/ImageProviders.mjs b/examples/Image Colors/ImageProviders.mjs new file mode 100644 index 0000000..368881b --- /dev/null +++ b/examples/Image Colors/ImageProviders.mjs @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2024 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +export class ImageData { + constructor({ author_name, author_url, image_description, image_page, image_url }) { + this.author_name = author_name; + this.author_url = author_url; + this.image_description = image_description; + this.image_page = image_page; + this.image_url = image_url; + } +} + +export class FileProvider { + constructor(fileUrl) { + this.fileUrl = fileUrl; + } + + imageData() { + return new ImageData({ + author_name: "Unknown", + author_url: "", + image_description: decodeURI(this.fileUrl).replace(/.*\//, ""), + image_page: this.fileUrl, + image_url: this.fileUrl, + }); + } +} + +// A little bit obfuscated to prevent simple grep +const defaultUnsplashAccessKey = Qt.atob("LWlTR3FPbXJYeTY1LW9ncU1uNGtNe" + "TRlaDZXUmZSVVdDMGdrNllublh2Zw=="); + +export class UnsplashProvider { + constructor(accessKey = defaultUnsplashAccessKey, apiUrl = "https://api.unsplash.com") { + this.apiUrl = apiUrl; + this.accessKey = accessKey; + } + + // callback: (ImageData) -> void + getRandom(callback) { + const xhr = this.__openRequest("GET", "/photos/random"); + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { + const json = JSON.parse(xhr.response); + const imageData = new ImageData({ + author_name: json.user.name, + author_url: json.user.links.self, + image_description: json.alt_description, + image_page: json.links.html, + image_url: json.urls.regular, + }); + callback(imageData); + } + }; + xhr.send(); + return xhr; + } + + __openRequest(method, endpoint) { + const xhr = new XMLHttpRequest(); + xhr.open(method, `${this.apiUrl}${endpoint}`); + xhr.setRequestHeader("Accept-Version", "v1"); + xhr.setRequestHeader("Authorization", `Client-ID ${this.accessKey}`); + return xhr; + } +} diff --git a/examples/Image Colors/ImageStatsPage.qml b/examples/Image Colors/ImageStatsPage.qml new file mode 100644 index 0000000..70fddd1 --- /dev/null +++ b/examples/Image Colors/ImageStatsPage.qml @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2024 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +import "components" as Components + +Kirigami.ScrollablePage { + id: root + + required property Image image + required property Kirigami.ImageColors imagePalette + required property /*ImageProviders.ImageData*/ var imageData + required property Item overlay + + header: QQC2.ToolBar { + contentItem: Kirigami.ActionToolBar { + alignment: Qt.AlignHCenter + actions: [ + Kirigami.Action { + text: "Update Palette" + icon.name: "view-refresh-symbolic" + displayHint: Kirigami.DisplayHint.KeepVisible + onTriggered: source => { + imagePalette.update(); + } + } + ] + } + } + + ColumnLayout { + spacing: Kirigami.Units.gridUnit + + Components.Section { + title: "Image" + + Kirigami.UrlButton { + url: root.imageData?.image_page ?? "" + text: root.imageData?.image_description ?? "" + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Components.Section { + title: "Author" + + Kirigami.UrlButton { + url: root.imageData?.author_url ?? "" + text: root.imageData?.author_name ?? "" + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Components.Section { + title: "Controls" + + QQC2.CheckBox { + id: showOverlayCheckbox + Layout.fillWidth: true + text: "Show Overlay" + checked: root.overlay?.visible ?? false + onToggled: { + const overlay = root.overlay; + if (overlay) { + overlay.visible = checked; + } + } + } + } + + Components.Section { + title: "Image Palette" + + Components.PaletteTable { + swatches: imagePalette.palette + } + } + } +} diff --git a/examples/Image Colors/README.md b/examples/Image Colors/README.md new file mode 100644 index 0000000..7001652 --- /dev/null +++ b/examples/Image Colors/README.md @@ -0,0 +1,17 @@ +# Image Colors + +An application which analyzes icon and image colors. + +Images are kindly provided by [Unsplash](https://unsplash.com), and you might +need to update the access key in the [ImageProviders.mjs](./ImageProviders.mjs) +file if the one provided there stops working. + +## Run + +The entry point is `main.qml`, you can execute it with `qml6` CLI tool, or +with a `kqml` from plasma-sdk package. + +``` +$ qml6 -a widget main.qml +$ kqml main.qml +``` diff --git a/examples/Image Colors/components/ColorWell.qml b/examples/Image Colors/components/ColorWell.qml new file mode 100644 index 0000000..708e9f2 --- /dev/null +++ b/examples/Image Colors/components/ColorWell.qml @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2024 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +RowLayout { + id: root + + required property color color + + property bool showLabel: true + + default property alias __contentData: contentWrapper.data + + Layout.fillWidth: false + Layout.fillHeight: false + + Rectangle { + id: borderGraphics + + Layout.alignment: Qt.AlignVCenter | (root.showLabel ? Qt.AlignLeft : Qt.AlignHCenter) + Layout.fillWidth: !root.showLabel + Layout.fillHeight: !root.showLabel + + implicitWidth: 32 + implicitHeight: 32 + + border.width: 1 + border.color: Kirigami.Theme.textColor + color: "transparent" + radius: Kirigami.Units.cornerRadius + border.width + + Rectangle { + id: contentWrapper + z: -1 + anchors.fill: parent + anchors.margins: borderGraphics.border.width + border.width: 2 + border.color: Kirigami.Theme.backgroundColor + color: root.color + radius: Kirigami.Units.cornerRadius + } + + Image { + z: -2 + anchors.fill: contentWrapper + anchors.margins: contentWrapper.border.width + source: Qt.resolvedUrl("checkerboard.svg") + fillMode: Image.Tile + visible: root.color.a < 1.0 + } + } + + Kirigami.SelectableLabel { + Layout.fillWidth: true + Layout.fillHeight: true + visible: root.showLabel + verticalAlignment: TextEdit.AlignVCenter + text: root.color.toString() + } +} + diff --git a/examples/Image Colors/components/PaletteTable.qml b/examples/Image Colors/components/PaletteTable.qml new file mode 100644 index 0000000..4d1e5ca --- /dev/null +++ b/examples/Image Colors/components/PaletteTable.qml @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: 2024 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +ColumnLayout { + id: root + + required property list swatches + + spacing: Kirigami.Units.smallSpacing + + RowLayout { + spacing: Kirigami.Units.smallSpacing + + QQC2.Label { + text: "Color" + } + + Item { + Layout.fillWidth: true + } + + QQC2.Label { + text: "Contrast Color" + elide: Text.ElideRight + } + } + + Repeater { + model: root.swatches + + delegate: RowLayout { + id: delegate + + required property Kirigami.imageColorsPaletteSwatch modelData + + spacing: Kirigami.Units.smallSpacing + Layout.fillWidth: true + + Item { + implicitHeight: colorItem.height + Layout.fillWidth: true + + ColorWell { + id: colorItem + anchors.left: parent.left + width: Math.max(12, parent.width * delegate.modelData.ratio) + color: delegate.modelData.color + showLabel: false + } + + ShadowedLabel { + id: label + + anchors { + top: parent.top + left: colorItem.right + bottom: parent.bottom + } + verticalAlignment: Text.AlignVCenter + text: `${(delegate.modelData.ratio * 100).toFixed(2)}%` + + states: [ + State { + when: delegate.modelData.ratio > 0.66 + name: "inside" + + PropertyChanges { + target: label + anchors.leftMargin: - label.width - Kirigami.Units.largeSpacing + layer.enabled: true + // The contrast color sometimes isn't really usable + // color: delegate.modelData.contrastColor + } + }, + State { + when: true + name: "side-by-side" + + PropertyChanges { + target: label + anchors.leftMargin: Kirigami.Units.smallSpacing + } + } + ] + } + } + ColorWell { + Layout.fillWidth: false + Layout.alignment: Qt.AlignRight + color: delegate.modelData.contrastColor + showLabel: false + } + } + } +} diff --git a/examples/Image Colors/components/Section.qml b/examples/Image Colors/components/Section.qml new file mode 100644 index 0000000..fd63661 --- /dev/null +++ b/examples/Image Colors/components/Section.qml @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2024 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +ColumnLayout { + id: root + + property string title + + spacing: 0 + Layout.fillWidth: true + + Kirigami.Heading { + Layout.fillWidth: true + level: 3 + text: root.title + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + + Kirigami.Separator { + Layout.fillWidth: true + Layout.topMargin: Kirigami.Units.smallSpacing + Layout.bottomMargin: Kirigami.Units.largeSpacing + } +} diff --git a/examples/Image Colors/components/ShadowedLabel.qml b/examples/Image Colors/components/ShadowedLabel.qml new file mode 100644 index 0000000..6d92f01 --- /dev/null +++ b/examples/Image Colors/components/ShadowedLabel.qml @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2024 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls as QQC2 +import Qt5Compat.GraphicalEffects as GE + +QQC2.Label { + layer.effect: GE.DropShadow { + verticalOffset: 1 + radius: 4.0 + samples: radius * 2 + 1 + spread: 0.35 + color: "black" + } +} diff --git a/examples/Image Colors/components/checkerboard.svg b/examples/Image Colors/components/checkerboard.svg new file mode 100644 index 0000000..8622702 --- /dev/null +++ b/examples/Image Colors/components/checkerboard.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/Image Colors/main.qml b/examples/Image Colors/main.qml new file mode 100644 index 0000000..6649af3 --- /dev/null +++ b/examples/Image Colors/main.qml @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2024 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +import "components" as Components + +QQC2.ApplicationWindow { + id: root + + width: 800 + height: 600 + minimumWidth: 600 + visible: true + + QQC2.SplitView { + id: splitView + anchors.fill: parent + + IconPage { + QQC2.SplitView.fillWidth: false + QQC2.SplitView.preferredWidth: Kirigami.Units.gridUnit * 12 + QQC2.SplitView.minimumWidth: Kirigami.Units.gridUnit * 10 + clip: true + } + + ImagePage { + id: imagePage + + QQC2.SplitView.fillWidth: true + QQC2.SplitView.minimumWidth: Kirigami.Units.gridUnit * 10 + clip: true + } + + ImageStatsPage { + QQC2.SplitView.fillWidth: false + QQC2.SplitView.preferredWidth: Kirigami.Units.gridUnit * 12 + QQC2.SplitView.minimumWidth: Kirigami.Units.gridUnit * 10 + clip: true + + image: imagePage.image + imagePalette: imagePage.imagePalette + imageData: imagePage.imageData + overlay: imagePage.overlay + } + } +} diff --git a/examples/applicationitemapp/CMakeLists.txt b/examples/applicationitemapp/CMakeLists.txt new file mode 100644 index 0000000..7a936e0 --- /dev/null +++ b/examples/applicationitemapp/CMakeLists.txt @@ -0,0 +1,18 @@ +find_package(Qt6 ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE COMPONENTS Widgets) + +set(applicationitemapp_SRCS + main.cpp +) + +qt_add_resources(RESOURCES resources.qrc) + +add_executable(applicationitemapp ${applicationitemapp_SRCS} ${RESOURCES}) +target_link_libraries(applicationitemapp + Qt6::Core + Qt6::Qml + Qt6::Quick + Qt6::Svg + Qt6::Widgets +) + +install(TARGETS applicationitemapp ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/examples/applicationitemapp/main.cpp b/examples/applicationitemapp/main.cpp new file mode 100644 index 0000000..e54bf41 --- /dev/null +++ b/examples/applicationitemapp/main.cpp @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ +#include +#include +#ifdef Q_OS_ANDROID +#include +#else +#include +#endif + +Q_DECL_EXPORT int main(int argc, char *argv[]) +{ +#ifdef Q_OS_ANDROID + QGuiApplication app(argc, argv); +#else + QApplication app(argc, argv); +#endif + + QQuickView view; + view.setResizeMode(QQuickView::SizeRootObjectToView); + view.setSource(QUrl(QStringLiteral("qrc:///main.qml"))); + view.show(); + + return app.exec(); +} diff --git a/examples/applicationitemapp/main.qml b/examples/applicationitemapp/main.qml new file mode 100644 index 0000000..708dfcc --- /dev/null +++ b/examples/applicationitemapp/main.qml @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationItem { + id: root + + globalDrawer: Kirigami.GlobalDrawer { + actions: [ + Kirigami.Action { + text: "View" + icon.name: "view-list-icons" + Kirigami.Action { + text: "action 1" + } + Kirigami.Action { + text: "action 2" + } + Kirigami.Action { + text: "action 3" + } + }, + Kirigami.Action { + text: "action 3" + }, + Kirigami.Action { + text: "action 4" + } + ] + handleVisible: true + } + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + + actions: (pageStack.currentItem as Kirigami.Page)?.actions ?? [] + } + + pageStack.initialPage: mainPageComponent + + Component { + id: mainPageComponent + Kirigami.Page { + title: "Hello" + actions: [ + Kirigami.Action { + text: "action 1" + }, + Kirigami.Action { + text: "action 2" + } + ] + Rectangle { + color: "#aaff7f" + anchors.fill: parent + } + } + } +} diff --git a/examples/applicationitemapp/resources.qrc b/examples/applicationitemapp/resources.qrc new file mode 100644 index 0000000..89f4bbe --- /dev/null +++ b/examples/applicationitemapp/resources.qrc @@ -0,0 +1,5 @@ + + + main.qml + + diff --git a/examples/flexcolumn/main.qml b/examples/flexcolumn/main.qml new file mode 100644 index 0000000..65c6a6f --- /dev/null +++ b/examples/flexcolumn/main.qml @@ -0,0 +1,30 @@ +import QtQuick +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +Kirigami.FlexColumn { + Rectangle { + color: "red" + + Layout.preferredHeight: 200 + Layout.fillWidth: true + } + Rectangle { + color: "orange" + + Layout.preferredHeight: 100 + Layout.fillWidth: true + } + Rectangle { + color: "yellow" + + Layout.preferredHeight: 50 + Layout.fillWidth: true + } + Rectangle { + color: "green" + + Layout.preferredHeight: 25 + Layout.fillWidth: true + } +} \ No newline at end of file diff --git a/examples/icon/CustomSource.qml b/examples/icon/CustomSource.qml new file mode 100644 index 0000000..4e88532 --- /dev/null +++ b/examples/icon/CustomSource.qml @@ -0,0 +1,5 @@ +import org.kde.kirigami as Kirigami + +Kirigami.Icon { + source: "image://provider/kirigami.svg" +} diff --git a/examples/icon/Fallback.qml b/examples/icon/Fallback.qml new file mode 100644 index 0000000..d9c60a2 --- /dev/null +++ b/examples/icon/Fallback.qml @@ -0,0 +1,6 @@ +import org.kde.kirigami as Kirigami + +Kirigami.Icon { + source: "this-icon-does-not-exist" + fallback: "view-refresh" +} diff --git a/examples/icon/FilesystemSource.qml b/examples/icon/FilesystemSource.qml new file mode 100644 index 0000000..78dda83 --- /dev/null +++ b/examples/icon/FilesystemSource.qml @@ -0,0 +1,5 @@ +import org.kde.kirigami as Kirigami + +Kirigami.Icon { + source: "/home/example/cool.svg" +} diff --git a/examples/icon/IconThemeSource.qml b/examples/icon/IconThemeSource.qml new file mode 100644 index 0000000..cd53c43 --- /dev/null +++ b/examples/icon/IconThemeSource.qml @@ -0,0 +1,5 @@ +import org.kde.kirigami as Kirigami + +Kirigami.Icon { + source: "view-refresh" +} diff --git a/examples/icon/InternetSource.qml b/examples/icon/InternetSource.qml new file mode 100644 index 0000000..84526d6 --- /dev/null +++ b/examples/icon/InternetSource.qml @@ -0,0 +1,5 @@ +import org.kde.kirigami as Kirigami + +Kirigami.Icon { + source: "https://example.com/kirigami.png" +} diff --git a/examples/icon/ResourceSource.qml b/examples/icon/ResourceSource.qml new file mode 100644 index 0000000..e308996 --- /dev/null +++ b/examples/icon/ResourceSource.qml @@ -0,0 +1,5 @@ +import org.kde.kirigami as Kirigami + +Kirigami.Icon { + source: "qrc:/kirigami.svg" +} diff --git a/examples/multiplatformnotesapp/NotesGeneral.qml b/examples/multiplatformnotesapp/NotesGeneral.qml new file mode 100644 index 0000000..6ad2b68 --- /dev/null +++ b/examples/multiplatformnotesapp/NotesGeneral.qml @@ -0,0 +1,203 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import org.kde.kirigami.delegates as KD + +Kirigami.ApplicationWindow { + id: root + + property string currentFile + + pageStack.initialPage: iconView + + Kirigami.ScrollablePage { + id: iconView + title: "Notes" + actions: Kirigami.Action { + id: sortAction + icon.name: "view-sort-ascending-symbolic" + tooltip: "Sort Ascending" + } + + background: Rectangle { + color: Kirigami.Theme.backgroundColor + } + + GridView { + id: view + model: 100 + cellWidth: Kirigami.Units.gridUnit * 9 + cellHeight: cellWidth + currentIndex: -1 + highlightMoveDuration: 0 + highlight: Rectangle { + color: Kirigami.Theme.highlightColor + } + delegate: MouseArea { + id: delegate + + required property int index + required property int modelData + + width: view.cellWidth + height: view.cellHeight + Kirigami.Icon { + source: "text-plain" + anchors { + fill: parent + margins: Kirigami.Units.gridUnit + } + QQC2.Label { + anchors { + top: parent.bottom + horizontalCenter: parent.horizontalCenter + } + text: "File " + delegate.modelData + } + } + onClicked: { + view.currentIndex = index; + root.currentFile = "File " + delegate.modelData; + if (root.pageStack.depth < 2) { + root.pageStack.push(editorComponent); + } + root.pageStack.currentIndex = 1 + } + } + } + } + + Component { + id: editorComponent + Kirigami.ScrollablePage { + id: editor + title: root.currentFile + actions: [ + Kirigami.Action { + id: shareAction + icon.name: "document-share" + text: "Share…" + tooltip: "Share this document with your device" + checked: sheet.visible + checkable: true + onCheckedChanged: checked => { + sheet.visible = checked; + } + }, + Kirigami.Action { + icon.name: "format-text-bold-symbolic" + tooltip: "Bold" + }, + Kirigami.Action { + icon.name: "format-text-underline-symbolic" + tooltip: "Underline" + }, + Kirigami.Action { + icon.name: "format-text-italic-symbolic" + tooltip: "Italic" + } + ] + background: Rectangle { + color: Kirigami.ColorUtils.brightnessForColor(Kirigami.Theme.backgroundColor) === Kirigami.ColorUtils.Light + ? "#F8EBC3" : "#1F2226" + } + + Kirigami.OverlaySheet { + id: sheet + + parent: root.QQC2.Overlay.overlay + + onOpened: forceActiveFocus(Qt.PopupFocusReason) + + ListView { + implicitWidth: Kirigami.Units.gridUnit * 30 + reuseItems: true + model: ListModel { + ListElement { + title: "Share with phone \"Nokia 3310\"" + description: "You selected this phone 12 times before. It's currently connected via bluetooth" + buttonText: "Push Sync" + } + ListElement { + title: "Share with phone \"My other Nexus5\"" + description: "You selected this phone 0 times before. It's currently connected to your laptop via Wifi" + buttonText: "Push Sync" + } + ListElement { + title: "Share with NextCloud" + description: "You currently do not have a server set up for sharing and storing notes from Katie. If you want to set one up click here" + buttonText: "Setup…" + } + ListElement { + title: "Send document via email" + description: "This will send the document as an attached file to your own email for later sync" + buttonText: "Send As Email" + } + } + header: KD.SubtitleDelegate { + hoverEnabled: false + down: false + + width: ListView.view?.width + + text: "This document has already automatically synced with your phone \"Dancepartymeister 12\". If you want to sync with another device or do further actions you can do that here" + + icon.name: "documentinfo" + icon.width: Kirigami.Units.iconSizes.large + icon.height: Kirigami.Units.iconSizes.large + } + delegate: QQC2.ItemDelegate { + id: delegate + + required property string title + required property string description + required property string buttonText + + hoverEnabled: false + highlighted: false + down: false + + width: ListView.view?.width + + contentItem: RowLayout { + spacing: Kirigami.Units.smallSpacing + KD.TitleSubtitle { + Layout.fillWidth: true + title: delegate.title + subtitle: delegate.description + } + QQC2.Button { + text: delegate.buttonText + onClicked: sheet.close() + } + } + } + } + } + + QQC2.TextArea { + background: null + wrapMode: TextEdit.Wrap + selectByMouse: true + text: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sollicitudin, lorem at semper pretium, tortor nisl pellentesque risus, eget eleifend odio ipsum ac mi. Donec justo ex, elementum vitae gravida vel, pretium ac lacus. Duis non metus ac enim viverra auctor in non nunc. Sed sit amet luctus nisi. Proin justo nulla, vehicula eget porta sit amet, aliquet vitae dolor. Mauris sed odio auctor, tempus ipsum ac, placerat enim. Ut in dolor vel ante dictum auctor. + + Praesent blandit rhoncus augue. Phasellus consequat luctus pulvinar. Pellentesque rutrum laoreet dolor, sit amet pellentesque tellus mattis sed. Sed accumsan cursus tortor. Morbi et risus dolor. Nullam facilisis ipsum justo, nec sollicitudin mi pulvinar ac. Nulla facilisi. Donec maximus turpis eget mollis laoreet. Phasellus vel mauris et est mattis auctor eget sit amet turpis. Aliquam dignissim euismod purus, eu efficitur neque fermentum eu. Suspendisse potenti. Praesent mattis ex vitae neque rutrum tincidunt. Etiam placerat leo viverra pulvinar tincidunt. + + Proin vel rutrum massa. Proin volutpat aliquet dapibus. Maecenas aliquet elit eu venenatis venenatis. Ut elementum, lacus vel auctor auctor, velit massa elementum ligula, quis elementum ex nisi aliquam mauris. Nulla facilisi. Pellentesque aliquet egestas venenatis. Donec iaculis ultrices laoreet. Vestibulum cursus rhoncus sollicitudin. + + Proin quam libero, bibendum eget sodales id, gravida quis enim. Duis fermentum libero vitae sapien hendrerit, in tincidunt tortor semper. Nullam quam nisi, feugiat sed rutrum vitae, dignissim quis risus. Ut ultricies pellentesque est, ut gravida massa convallis sed. Ut placerat dui non felis interdum, id malesuada nulla ornare. Phasellus volutpat purus placerat velit porta tristique. Donec molestie leo in turpis bibendum pharetra. Fusce fermentum diam vitae neque laoreet, sed aliquam leo sollicitudin. + + Ut facilisis massa arcu, eu suscipit ante varius sed. Morbi augue leo, mattis eu tempor vitae, condimentum sed urna. Curabitur ac blandit orci. Vestibulum quis consequat nunc. Proin imperdiet commodo imperdiet. Aenean mattis augue et imperdiet ultricies. Ut id feugiat nulla, et sollicitudin dui. Etiam scelerisque ligula ac euismod hendrerit. Integer in quam nibh. Pellentesque risus massa, porttitor quis fermentum eu, dictum varius magna. Morbi euismod bibendum lacus efficitur pretium. Phasellus elementum porttitor enim nec dictum. Morbi et augue laoreet, convallis quam quis, egestas quam.` + } + } + } +} diff --git a/examples/multiplatformnotesapp/notesDesktop.qml b/examples/multiplatformnotesapp/notesDesktop.qml new file mode 100644 index 0000000..0b988da --- /dev/null +++ b/examples/multiplatformnotesapp/notesDesktop.qml @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +NotesGeneral { + id: root +} diff --git a/examples/multiplatformnotesapp/notesMobile.qml b/examples/multiplatformnotesapp/notesMobile.qml new file mode 100644 index 0000000..587fd59 --- /dev/null +++ b/examples/multiplatformnotesapp/notesMobile.qml @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import org.kde.kirigami as Kirigami + +NotesGeneral { + id: root + + contextDrawer: Kirigami.ContextDrawer {} +} diff --git a/examples/settingscomponents/GeneralSettingsPage.qml b/examples/settingscomponents/GeneralSettingsPage.qml new file mode 100644 index 0000000..abf76ce --- /dev/null +++ b/examples/settingscomponents/GeneralSettingsPage.qml @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2021 Felipe Kinoshita + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +Kirigami.ScrollablePage { + title: qsTr("General") + + QQC2.CheckBox { + Kirigami.FormData.label: i18n("Something") + text: i18n("Do something") + } +} diff --git a/examples/settingscomponents/SettingsPage.qml b/examples/settingscomponents/SettingsPage.qml new file mode 100644 index 0000000..4deb52e --- /dev/null +++ b/examples/settingscomponents/SettingsPage.qml @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2021 Felipe Kinoshita + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.settings as AddonsSettings + +AddonsSettings.CategorizedSettings { + actions: [ + AddonsSettings.SettingAction { + text: qsTr("General") + actionName: qsTr("General") + icon.name: "wayland" + page: Qt.resolvedUrl("./GeneralSettingsPage.qml") + } + ] +} diff --git a/examples/settingscomponents/main.qml b/examples/settingscomponents/main.qml new file mode 100644 index 0000000..16675ea --- /dev/null +++ b/examples/settingscomponents/main.qml @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2021 Felipe Kinoshita + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: root + + title: qsTr("Hello, World") + + globalDrawer: Kirigami.GlobalDrawer { + isMenu: !Kirigami.isMobile + actions: [ + Kirigami.Action { + text: qsTr("Settings") + icon.name: "settings-configure" + onTriggered: root.pageStack.pushDialogLayer(Qt.resolvedUrl("./SettingsPage.qml"), { + width: root.width + }, { + title: qsTr("Settings"), + width: root.width - (Kirigami.Units.gridUnit * 4), + height: root.height - (Kirigami.Units.gridUnit * 4) + }) + } + ] + } + + pageStack.initialPage: Kirigami.Page { + title: qsTr("Main Page") + } +} diff --git a/examples/settingscomponents/resources.qrc b/examples/settingscomponents/resources.qrc new file mode 100644 index 0000000..a63d9d2 --- /dev/null +++ b/examples/settingscomponents/resources.qrc @@ -0,0 +1,7 @@ + + + main.qml + SettingsPage.qml + GeneralSettingsPage.qml + + diff --git a/examples/simpleexamples/AbstractApplicationWindow.qml b/examples/simpleexamples/AbstractApplicationWindow.qml new file mode 100644 index 0000000..444373e --- /dev/null +++ b/examples/simpleexamples/AbstractApplicationWindow.qml @@ -0,0 +1,149 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +Kirigami.AbstractApplicationWindow { + id: root + + width: 500 + height: 800 + visible: true + + globalDrawer: Kirigami.GlobalDrawer { + title: "Widget gallery" + titleIcon: "applications-graphics" + actions: [ + Kirigami.Action { + text: "View" + icon.name: "view-list-icons" + Kirigami.Action { + text: "action 1" + } + Kirigami.Action { + text: "action 2" + } + Kirigami.Action { + text: "action 3" + } + }, + Kirigami.Action { + text: "Sync" + icon.name: "folder-sync" + Kirigami.Action { + text: "action 4" + } + Kirigami.Action { + text: "action 5" + } + }, + Kirigami.Action { + text: "Checkable" + icon.name: "view-list-details" + checkable: true + checked: false + onTriggered: { + print("Action checked:" + checked) + } + }, + Kirigami.Action { + text: "Settings" + icon.name: "configure" + checkable: true + //Need to do this, otherwise it breaks the bindings + property bool current: pageStack.currentItem?.objectName === "settingsPage" ?? false + onCurrentChanged: { + checked = current; + } + onTriggered: { + pageStack.push(settingsComponent); + } + } + ] + + QQC2.CheckBox { + checked: true + text: "Option 1" + } + QQC2.CheckBox { + text: "Option 2" + } + QQC2.CheckBox { + text: "Option 3" + } + QQC2.Slider { + Layout.fillWidth: true + value: 0.5 + } + } + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + } + + pageStack: QQC2.StackView { + anchors.fill: parent + property int currentIndex: 0 + focus: true + onCurrentIndexChanged: { + if (depth > currentIndex + 1) { + pop(get(currentIndex)); + } + } + onDepthChanged: { + currentIndex = depth-1; + } + initialItem: mainPageComponent + + Keys.onReleased: event => { + if (event.key === Qt.Key_Back || + (event.key === Qt.Key_Left && (event.modifiers & Qt.AltModifier))) { + event.accepted = true; + if (root.contextDrawer && root.contextDrawer.drawerOpen) { + root.contextDrawer.close(); + } else if (root.globalDrawer && root.globalDrawer.drawerOpen) { + root.globalDrawer.close(); + } else { + var backEvent = {accepted: false} + if (root.pageStack.currentIndex >= 1) { + root.pageStack.currentItem.backRequested(backEvent); + if (!backEvent.accepted) { + if (root.pageStack.depth > 1) { + root.pageStack.currentIndex = Math.max(0, root.pageStack.currentIndex - 1); + backEvent.accepted = true; + } else { + Qt.quit(); + } + } + } + + if (!backEvent.accepted) { + Qt.quit(); + } + } + } + } + } + + Component { + id: settingsComponent + Kirigami.Page { + title: "Settings" + objectName: "settingsPage" + Rectangle { + anchors.fill: parent + } + } + } + + //Main app content + Component { + id: mainPageComponent + MultipleColumnsGallery {} + } +} diff --git a/examples/simpleexamples/FixedSidebar.qml b/examples/simpleexamples/FixedSidebar.qml new file mode 100644 index 0000000..16c8cc6 --- /dev/null +++ b/examples/simpleexamples/FixedSidebar.qml @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import org.kde.kirigami as Kirigami +import QtQuick.Controls as QQC2 + +Kirigami.ApplicationWindow { + id: root + + width: Kirigami.Units.gridUnit * 60 + height: Kirigami.Units.gridUnit * 40 + + pageStack.initialPage: mainPageComponent + globalDrawer: Kirigami.OverlayDrawer { + drawerOpen: true + modal: false + contentItem: Item { + implicitWidth: Kirigami.Units.gridUnit * 10 + + QQC2.Label { + text: "This is a sidebar" + width: parent.width - Kirigami.Units.smallSpacing * 2 + wrapMode: Text.WordWrap + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + //Main app content + Component { + id: mainPageComponent + MultipleColumnsGallery {} + } +} diff --git a/examples/simpleexamples/MultipleColumnsGallery.qml b/examples/simpleexamples/MultipleColumnsGallery.qml new file mode 100644 index 0000000..c047b3e --- /dev/null +++ b/examples/simpleexamples/MultipleColumnsGallery.qml @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +Kirigami.ScrollablePage { + id: page + + Layout.fillWidth: true + implicitWidth: Kirigami.Units.gridUnit * (Math.floor(Math.random() * 35) + 8) + + title: "Multiple Columns" + + actions: [ + Kirigami.Action { + text:"Action for buttons" + icon.name: "bookmarks" + onTriggered: print("Action 1 clicked") + }, + Kirigami.Action { + text:"Action 2" + icon.name: "folder" + enabled: false + } + ] + + ColumnLayout { + width: page.width + spacing: Kirigami.Units.smallSpacing + + QQC2.Label { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: "This page is used to test multiple columns: you can push and pop an arbitrary number of pages, each new page will have a random implicit width between 8 and 35 grid units.\nIf you enlarge the window enough, you can test how the application behaves with multiple columns." + } + Item { + Layout.minimumWidth: Kirigami.Units.gridUnit *2 + Layout.minimumHeight: Layout.minimumWidth + } + QQC2.Label { + Layout.alignment: Qt.AlignHCenter + text: "Page implicitWidth: " + page.implicitWidth + } + QQC2.Button { + text: "Push Another Page" + Layout.alignment: Qt.AlignHCenter + onClicked: applicationWindow()?.pageStack.push(Qt.resolvedUrl("MultipleColumnsGallery.qml")); + } + QQC2.Button { + text: "Pop A Page" + Layout.alignment: Qt.AlignHCenter + onClicked: applicationWindow()?.pageStack.pop(); + } + RowLayout { + Layout.alignment: Qt.AlignHCenter + QQC2.TextField { + id: edit + text: page.title + } + QQC2.Button { + text: "Rename Page" + onClicked: page.title = edit.text; + } + } + Kirigami.SearchField { + id: searchField + Layout.alignment: Qt.AlignHCenter + onAccepted: console.log("Search text is " + text); + } + Kirigami.PasswordField { + id: passwordField + Layout.alignment: Qt.AlignHCenter + onAccepted: console.log("Password") + } + } +} diff --git a/examples/simpleexamples/Sidebar.qml b/examples/simpleexamples/Sidebar.qml new file mode 100644 index 0000000..e230d1e --- /dev/null +++ b/examples/simpleexamples/Sidebar.qml @@ -0,0 +1,168 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: root + width: Kirigami.Units.gridUnit * 60 + height: Kirigami.Units.gridUnit * 40 + + + pageStack.initialPage: mainPageComponent + globalDrawer: Kirigami.OverlayDrawer { + id: drawer + drawerOpen: true + modal: false + //leftPadding: Kirigami.Units.largeSpacing + rightPadding: Kirigami.Units.largeSpacing + contentItem: ColumnLayout { + Layout.preferredWidth: Kirigami.Units.gridUnit * 20 + + QQC2.Label { + Layout.alignment: Qt.AlignHCenter + text: "This is a sidebar" + Layout.fillWidth: true + width: parent.width - Kirigami.Units.smallSpacing * 2 + wrapMode: Text.WordWrap + } + QQC2.Button { + Layout.alignment: Qt.AlignHCenter + text: "Modal" + checkable: true + Layout.fillWidth: true + checked: false + onCheckedChanged: drawer.modal = checked + } + Item { + Layout.fillHeight: true + } + } + } + contextDrawer: Kirigami.OverlayDrawer { + id: contextDrawer + drawerOpen: true + edge: Qt.application.layoutDirection === Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge + modal: false + leftPadding: Kirigami.Units.largeSpacing + rightPadding: Kirigami.Units.largeSpacing + contentItem: ColumnLayout { + Layout.preferredWidth: Kirigami.Units.gridUnit * 10 + + QQC2.Label { + Layout.alignment: Qt.AlignHCenter + text: "This is a sidebar" + Layout.fillWidth: true + width: parent.width - Kirigami.Units.smallSpacing * 2 + wrapMode: Text.WordWrap + } + QQC2.Button { + Layout.alignment: Qt.AlignHCenter + text: "Modal" + checkable: true + Layout.fillWidth: true + checked: false + onCheckedChanged: contextDrawer.modal = checked + } + Item { + Layout.fillHeight: true + } + } + } + + menuBar: QQC2.MenuBar { + QQC2.Menu { + title: qsTr("&File") + QQC2.Action { text: qsTr("&New...") } + QQC2.Action { text: qsTr("&Open...") } + QQC2.Action { text: qsTr("&Save") } + QQC2.Action { text: qsTr("Save &As...") } + QQC2.MenuSeparator { } + QQC2.Action { text: qsTr("&Quit") } + } + QQC2.Menu { + title: qsTr("&Edit") + QQC2.Action { text: qsTr("Cu&t") } + QQC2.Action { text: qsTr("&Copy") } + QQC2.Action { text: qsTr("&Paste") } + } + QQC2.Menu { + title: qsTr("&Help") + QQC2.Action { text: qsTr("&About") } + } + } + header: QQC2.ToolBar { + contentItem: RowLayout { + QQC2.ToolButton { + text: "Global ToolBar" + } + Item { + Layout.fillWidth: true + } + Kirigami.ActionTextField { + id: searchField + + placeholderText: "Search…" + + focusSequence: StandardKey.Find + leftActions: [ + Kirigami.Action { + icon.name: "edit-clear" + visible: searchField.text.length > 0 + onTriggered: { + searchField.text = "" + searchField.accepted() + } + }, + Kirigami.Action { + icon.name: "edit-clear" + visible: searchField.text.length > 0 + onTriggered: { + searchField.text = "" + searchField.accepted() + } + } + ] + rightActions: [ + Kirigami.Action { + icon.name: "edit-clear" + visible: searchField.text.length > 0 + onTriggered: { + searchField.text = "" + searchField.accepted() + } + }, + Kirigami.Action { + icon.name: "anchor" + visible: searchField.text.length > 0 + onTriggered: { + searchField.text = "" + searchField.accepted() + } + } + ] + + onAccepted: console.log("Search text is " + searchField.text) + } + } + } + //Main app content + Component { + id: mainPageComponent + MultipleColumnsGallery {} + } + footer: QQC2.ToolBar { + position: QQC2.ToolBar.Footer + QQC2.Label { + anchors.fill: parent + verticalAlignment: Qt.AlignVCenter + text: "Global Footer" + } + } +} diff --git a/examples/simpleexamples/SimplePage.qml b/examples/simpleexamples/SimplePage.qml new file mode 100644 index 0000000..951a9ac --- /dev/null +++ b/examples/simpleexamples/SimplePage.qml @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +Kirigami.ScrollablePage { + id: page + + Layout.fillWidth: true + implicitWidth: Kirigami.Units.gridUnit * (Math.floor(Math.random() * 35) + 8) + + title: i18n("Simple Scrollable Page") + + actions: [ + Kirigami.Action { + text:"Action for buttons" + icon.name: "bookmarks" + onTriggered: print("Action 1 clicked") + }, + Kirigami.Action { + text:"Action 2" + icon.name: "folder" + enabled: false + } + ] + + ColumnLayout { + width: page.width + spacing: Kirigami.Units.smallSpacing + + QQC2.Label { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed a sem venenatis, dictum odio vitae, tincidunt sapien. Proin a suscipit ligula, id interdum leo. Donec sed dolor sed lacus dignissim tempor a a lorem. In ullamcorper varius vestibulum. Sed nec arcu semper, varius velit ut, pharetra est. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Integer odio nibh, tincidunt quis condimentum quis, consequat id lacus. Nulla quis mauris erat. Suspendisse rhoncus suscipit massa, at suscipit lorem rhoncus et." + } + } +} diff --git a/examples/simpleexamples/customdrawer.qml b/examples/simpleexamples/customdrawer.qml new file mode 100644 index 0000000..8e89bca --- /dev/null +++ b/examples/simpleexamples/customdrawer.qml @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: root + + globalDrawer: Kirigami.OverlayDrawer { + contentItem: Rectangle { + implicitWidth: Kirigami.Units.gridUnit * 10 + color: "red" + anchors.fill: parent + } + } + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + } + + pageStack.initialPage: mainPageComponent + + Component { + id: mainPageComponent + Kirigami.ScrollablePage { + title: "Hello" + Rectangle { + anchors.fill: parent + } + } + } +} diff --git a/examples/simpleexamples/dragPageWidth.qml b/examples/simpleexamples/dragPageWidth.qml new file mode 100644 index 0000000..587b77d --- /dev/null +++ b/examples/simpleexamples/dragPageWidth.qml @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2017 Eike Hein + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: root + + property int defaultColumnWidth: Kirigami.Units.gridUnit * 13 + property int columnWidth: defaultColumnWidth + + pageStack.defaultColumnWidth: columnWidth + pageStack.initialPage: [firstPageComponent, secondPageComponent] + + MouseArea { + id: dragHandle + + visible: pageStack.wideMode + + anchors.top: parent.top + anchors.bottom: parent.bottom + + x: columnWidth - (width / 2) + width: 2 + + property int dragRange: (Kirigami.Units.gridUnit * 5) + property int _lastX: -1 + + cursorShape: Qt.SplitHCursor + + onPressed: mouse => { + _lastX = mouse.x; + } + + onPositionChanged: mouse => { + if (mouse.x > _lastX) { + columnWidth = Math.min((defaultColumnWidth + dragRange), + columnWidth + (mouse.x - _lastX)); + } else if (mouse.x < _lastX) { + columnWidth = Math.max((defaultColumnWidth - dragRange), + columnWidth - (_lastX - mouse.x)); + } + } + + Rectangle { + anchors.fill: parent + + color: "blue" + } + } + + Component { + id: firstPageComponent + + Kirigami.Page { + id: firstPage + + background: Rectangle { color: "red" } + } + } + + Component { + id: secondPageComponent + + Kirigami.Page { + id: secondPage + + background: Rectangle { color: "green" } + } + } +} diff --git a/examples/simpleexamples/footer.qml b/examples/simpleexamples/footer.qml new file mode 100644 index 0000000..ebf532a --- /dev/null +++ b/examples/simpleexamples/footer.qml @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: root + + footer: QQC2.ToolBar { + //height: Kirigami.Units.gridUnit * 3 + RowLayout { + QQC2.ToolButton { + text: "text" + } + } + } + globalDrawer: Kirigami.GlobalDrawer { + actions: [ + Kirigami.Action { + text: "View" + icon.name: "view-list-icons" + Kirigami.Action { + text: "action 1" + } + Kirigami.Action { + text: "action 2" + } + Kirigami.Action { + text: "action 3" + } + }, + Kirigami.Action { + text: "action 3" + }, + Kirigami.Action { + text: "action 4" + } + ] + } + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + } + + pageStack.initialPage: mainPageComponent + + Component { + id: mainPageComponent + Kirigami.ScrollablePage { + title: "Hello" + Rectangle { + anchors.fill: parent + } + } + } + + +} diff --git a/examples/simpleexamples/layerOverDrawer.qml b/examples/simpleexamples/layerOverDrawer.qml new file mode 100644 index 0000000..5230648 --- /dev/null +++ b/examples/simpleexamples/layerOverDrawer.qml @@ -0,0 +1,67 @@ +/* + * SPDX-FileCopyrightText: 2023 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +/** + * This example show how to do a sidebar of an application what gets covered by new layers that might get pushed + * in the PageRow layer system. + * When a drawer is in sidebar mode, when it goes modal (for instance when on mobile) will still behave like a drawer + */ +Kirigami.ApplicationWindow { + id: root + + Kirigami.GlobalDrawer { + id: drawer + modal: false + + actions: [ + Kirigami.Action { + text: "Push Layer" + onTriggered: root.pageStack.layers.push(layerComponent) + }, + Kirigami.Action { + text: "Modal" + checked: drawer.modal + checkable: true + onTriggered: drawer.modal = checked + } + ] + } + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + } + + pageStack.initialPage: mainPageComponent + pageStack.leftSidebar: drawer + + Component { + id: mainPageComponent + Kirigami.ScrollablePage { + title: "Hello" + QQC2.Pane { + anchors.fill: parent + QQC2.Label { + text: "Main page: push a layer to cover both this and the sidebar" + } + } + } + } + Component { + id: layerComponent + Kirigami.ScrollablePage { + title: "Layer 1" + QQC2.Pane { + anchors.fill: parent + QQC2.Label { + text: "Layer page: this should cover the whole window: main page and sidebar" + } + } + } + } +} diff --git a/examples/simpleexamples/minimal.qml b/examples/simpleexamples/minimal.qml new file mode 100644 index 0000000..fda9a04 --- /dev/null +++ b/examples/simpleexamples/minimal.qml @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: root + + globalDrawer: Kirigami.GlobalDrawer { + actions: [ + Kirigami.Action { + text: "View" + icon.name: "view-list-icons" + Kirigami.Action { + text: "action 1" + } + Kirigami.Action { + text: "action 2" + } + Kirigami.Action { + text: "action 3" + } + }, + Kirigami.Action { + text: "action 3" + }, + Kirigami.Action { + text: "action 4" + } + ] + } + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + } + + pageStack.initialPage: mainPageComponent + + Component { + id: mainPageComponent + Kirigami.ScrollablePage { + title: "Hello" + Rectangle { + anchors.fill: parent + } + } + } +} diff --git a/examples/simpleexamples/pagePoolDrawer.qml b/examples/simpleexamples/pagePoolDrawer.qml new file mode 100644 index 0000000..2b95fc3 --- /dev/null +++ b/examples/simpleexamples/pagePoolDrawer.qml @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: root + + Kirigami.PagePool { + id: mainPagePool + } + + globalDrawer: Kirigami.GlobalDrawer { + modal: !root.wideScreen + width: Kirigami.Units.gridUnit * 10 + + actions: [ + Kirigami.PagePoolAction { + text: i18n("Page1") + icon.name: "speedometer" + pagePool: mainPagePool + page: "SimplePage.qml" + }, + Kirigami.PagePoolAction { + text: i18n("Page2") + icon.name: "window-duplicate" + pagePool: mainPagePool + page: "MultipleColumnsGallery.qml" + } + ] + } + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + } + + pageStack.initialPage: mainPagePool.loadPage("SimplePage.qml") + +} diff --git a/examples/simpleexamples/pagePoolFirstColumn.qml b/examples/simpleexamples/pagePoolFirstColumn.qml new file mode 100644 index 0000000..0dc8b1a --- /dev/null +++ b/examples/simpleexamples/pagePoolFirstColumn.qml @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: root + + Kirigami.PagePool { + id: mainPagePool + } + + globalDrawer: Kirigami.GlobalDrawer { + } + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + } + + pageStack.initialPage: wideScreen ? [firstPage, mainPagePool.loadPage("SimplePage.qml")] : [firstPage] + + Component { + id: firstPage + Kirigami.ScrollablePage { + id: root + title: i18n("Sidebar") + property list pageActions: [ + Kirigami.PagePoolAction { + text: i18n("Page1") + icon.name: "speedometer" + pagePool: mainPagePool + basePage: root + page: "SimplePage.qml" + }, + Kirigami.PagePoolAction { + text: i18n("Page2") + icon.name: "window-duplicate" + pagePool: mainPagePool + basePage: root + page: "MultipleColumnsGallery.qml" + } + ] + ListView { + model: pageActions + keyNavigationEnabled: true + activeFocusOnTab: true + reuseItems: true + delegate: QQC2.ItemDelegate { + id: delegate + action: modelData + width: parent.width + } + } + } + } +} diff --git a/examples/simpleexamples/pushpopclear.qml b/examples/simpleexamples/pushpopclear.qml new file mode 100644 index 0000000..eac83ab --- /dev/null +++ b/examples/simpleexamples/pushpopclear.qml @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: root + + globalDrawer: Kirigami.GlobalDrawer { + actions: [ + Kirigami.Action { + text: "push" + onTriggered: pageStack.push(secondPageComponent) + }, + Kirigami.Action { + text: "pop" + onTriggered: pageStack.pop() + }, + Kirigami.Action { + text: "clear" + onTriggered: pageStack.clear() + }, + Kirigami.Action { + text: "replace" + onTriggered: pageStack.replace(secondPageComponent) + } + ] + } + + pageStack.initialPage: mainPageComponent + + Component { + id: mainPageComponent + Kirigami.Page { + title: "First Page" + Rectangle { + anchors.fill: parent + QQC2.Label { + text: "First Page" + } + } + } + } + + Component { + id: secondPageComponent + Kirigami.Page { + title: "Second Page" + Rectangle { + color: "red" + anchors.fill: parent + QQC2.Label { + text: "Second Page" + } + } + } + } +} diff --git a/examples/simpleexamples/simpleChatApp.qml b/examples/simpleexamples/simpleChatApp.qml new file mode 100644 index 0000000..bbb06be --- /dev/null +++ b/examples/simpleexamples/simpleChatApp.qml @@ -0,0 +1,303 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: root + + //FIXME: perhaps the default logic for going widescreen should be refined upstream + wideScreen: width > columnWidth * 3 + property int columnWidth: Kirigami.Units.gridUnit * 13 + property int footerHeight: Math.round(Kirigami.Units.gridUnit * 2.5) + + globalDrawer: Kirigami.GlobalDrawer { + contentItem.implicitWidth: columnWidth + modal: true + drawerOpen: false + isMenu: true + + actions: [ + Kirigami.Action { + text: "Rooms" + icon.name: "view-list-icons" + }, + Kirigami.Action { + text: "Contacts" + icon.name: "tag-people" + }, + Kirigami.Action { + text: "Search" + icon.name: "search" + } + ] + } + contextDrawer: Kirigami.OverlayDrawer { + id: contextDrawer + //they can depend on the page like that or be defined directly here + edge: Qt.application.layoutDirection === Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge + modal: !root.wideScreen + onModalChanged: drawerOpen = !modal + handleVisible: root.controlsVisible + + //here padding 0 as listitems look better without as opposed to any other control + topPadding: 0 + bottomPadding: 0 + leftPadding: 0 + rightPadding: 0 + + contentItem: ColumnLayout { + readonly property int implicitWidth: root.columnWidth + spacing: 0 + QQC2.Control { + Layout.fillWidth: true + background: Rectangle { + anchors.fill: parent + color: Kirigami.Theme.highlightColor + opacity: 0.8 + } + + padding: Kirigami.Units.gridUnit + + contentItem: ColumnLayout { + id: titleLayout + spacing: Kirigami.Units.gridUnit + + RowLayout { + spacing: Kirigami.Units.gridUnit + Rectangle { + color: Kirigami.Theme.highlightedTextColor + radius: width + implicitWidth: Kirigami.Units.iconSizes.medium + implicitHeight: implicitWidth + } + ColumnLayout { + QQC2.Label { + Layout.fillWidth: true + color: Kirigami.Theme.highlightedTextColor + text: "KDE" + } + QQC2.Label { + Layout.fillWidth: true + color: Kirigami.Theme.highlightedTextColor + font: Kirigami.Theme.smallFont + text: "#kde: kde.org" + } + } + } + QQC2.Label { + Layout.fillWidth: true + color: Kirigami.Theme.highlightedTextColor + text: "Main room for KDE community, other rooms are listed at kde.org/rooms" + wrapMode: Text.WordWrap + } + } + } + + Kirigami.Separator { + Layout.fillWidth: true + } + + QQC2.ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + ListView { + reuseItems: true + model: 50 + delegate: QQC2.ItemDelegate { + text: "Person " + modelData + width: parent.width + } + } + } + + Kirigami.Separator { + Layout.fillWidth: true + Layout.maximumHeight: 1//implicitHeight + } + QQC2.ItemDelegate { + text: "Group call" + icon.name: "call-start" + width: parent.width + } + QQC2.ItemDelegate { + text: "Send Attachment" + icon.name: "mail-attachment" + width: parent.width + } + } + } + + pageStack.defaultColumnWidth: columnWidth + pageStack.initialPage: [channelsComponent, chatComponent] + + Component { + id: channelsComponent + Kirigami.ScrollablePage { + title: "Channels" + actions: Kirigami.Action { + icon.name: "search" + text: "Search" + } + background: Rectangle { + anchors.fill: parent + color: Kirigami.Theme.backgroundColor + } + footer: QQC2.ToolBar { + height: root.footerHeight + padding: Kirigami.Units.smallSpacing + RowLayout { + anchors.fill: parent + spacing: Kirigami.Units.smallSpacing + QQC2.ToolButton { + Layout.fillHeight: true + //make it square + implicitWidth: height + icon.name: "configure" + onClicked: root.pageStack.layers.push(secondLayerComponent); + } + QQC2.ComboBox { + Layout.fillWidth: true + Layout.fillHeight: true + model: ["First", "Second", "Third"] + } + } + } + ListView { + id: channelsList + currentIndex: 2 + model: 30 + reuseItems: true + delegate: QQC2.ItemDelegate { + text: "#Channel " + modelData + checkable: true + checked: channelsList.currentIndex === index + width: parent.width + } + } + } + } + + Component { + id: chatComponent + Kirigami.ScrollablePage { + title: "#KDE" + actions: [ + Kirigami.Action { + icon.name: "documentinfo" + text: "Channel info" + }, + Kirigami.Action { + icon.name: "search" + text: "Search" + }, + Kirigami.Action { + text: "Room Settings" + icon.name: "configure" + Kirigami.Action { + text: "Setting 1" + } + Kirigami.Action { + text: "Setting 2" + } + }, + Kirigami.Action { + text: "Shared Media" + icon.name: "document-share" + Kirigami.Action { + text: "Media 1" + } + Kirigami.Action { + text: "Media 2" + } + Kirigami.Action { + text: "Media 3" + } + } + ] + background: Rectangle { + anchors.fill: parent + color: Kirigami.Theme.backgroundColor + } + footer: QQC2.Control { + height: footerHeight + padding: Kirigami.Units.smallSpacing + background: Rectangle { + color: Kirigami.Theme.backgroundColor + Kirigami.Separator { + Rectangle { + anchors.fill: parent + color: Kirigami.Theme.focusColor + visible: chatTextInput.activeFocus + } + anchors { + left: parent.left + right: parent.right + top: parent.top + } + } + } + contentItem: RowLayout { + QQC2.TextField { + Layout.fillWidth: true + id: chatTextInput + background: Item {} + } + QQC2.ToolButton { + Layout.fillHeight: true + //make it square + implicitWidth: height + icon.name: "go-next" + } + } + } + + ListView { + id: channelsList + verticalLayoutDirection: ListView.BottomToTop + currentIndex: 2 + model: 30 + reuseItems: true + delegate: Item { + height: Kirigami.Units.gridUnit * 4 + ColumnLayout { + x: Kirigami.Units.gridUnit + anchors.verticalCenter: parent.verticalCenter + QQC2.Label { + text: modelData % 2 ? "John Doe" : "John Applebaum" + } + QQC2.Label { + text: "Message " + modelData + } + } + } + } + } + } + + Component { + id: secondLayerComponent + Kirigami.Page { + title: "Settings" + background: Rectangle { + color: Kirigami.Theme.backgroundColor + } + footer: QQC2.ToolBar { + height: root.footerHeight + QQC2.ToolButton { + Layout.fillHeight: true + //make it square + implicitWidth: height + icon.name: "configure" + onClicked: root.pageStack.layers.pop(); + } + } + } + } +} diff --git a/examples/staticcmake/3rdparty/CMakeLists.txt b/examples/staticcmake/3rdparty/CMakeLists.txt new file mode 100644 index 0000000..e10afb2 --- /dev/null +++ b/examples/staticcmake/3rdparty/CMakeLists.txt @@ -0,0 +1,3 @@ +set(BUILD_SHARED_LIBS 0) + +add_subdirectory(kirigami) diff --git a/examples/staticcmake/3rdparty/README b/examples/staticcmake/3rdparty/README new file mode 100644 index 0000000..aad19dd --- /dev/null +++ b/examples/staticcmake/3rdparty/README @@ -0,0 +1,6 @@ +Add here, with either a script that does a git checkout +or as git submodules the two projects: + +git://anongit.kde.org/kirigami.git +git://anongit.kde.org/breeze-icons.git + diff --git a/examples/staticcmake/CMakeLists.txt b/examples/staticcmake/CMakeLists.txt new file mode 100644 index 0000000..6d15556 --- /dev/null +++ b/examples/staticcmake/CMakeLists.txt @@ -0,0 +1,19 @@ +project(minimal) + +find_package(ECM REQUIRED CONFIG) +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/ ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH}) + +set(BREEZEICONS_DIR ${CMAKE_SOURCE_DIR}/3rdparty/breeze-icons/) + +find_package(Qt6 REQUIRED Core Quick Multimedia Test Widgets QuickControls2) + +include(KDEInstallDirs) +include(KDECompilerSettings) +include(KDECMakeSettings) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_subdirectory(3rdparty) +add_subdirectory(src) + diff --git a/examples/staticcmake/src/CMakeLists.txt b/examples/staticcmake/src/CMakeLists.txt new file mode 100644 index 0000000..c144161 --- /dev/null +++ b/examples/staticcmake/src/CMakeLists.txt @@ -0,0 +1,27 @@ + +include_directories(${CMAKE_SOURCE_DIR}/3rdparty/kirigami/src) +include(${CMAKE_SOURCE_DIR}/3rdparty/kirigami/KF6Kirigami2Macros.cmake) + +set(minimal_SRCS + main.cpp + ) + +qt_add_resources(RESOURCES kirigami-icons.qrc resources.qrc) + +if (ANDROID) + set(minimal_EXTRA_LIBS + #FIXME: we shouldn't have to link to it but otherwise the lib won't be packaged on Android + Qt6::QuickControls2) +else () +#qstyle-based qqc2 style needs a QApplication + set(minimal_EXTRA_LIBS Qt6::Widgets) +endif() + + +add_executable(minimal ${minimal_SRCS} ${RESOURCES}) +#kirigamiplugin is the static library built by us +target_link_libraries(minimal kirigamiplugin Qt6::Core Qt6::Qml Qt6::Quick Qt6::QuickControls2 ${minimal_EXTRA_LIBS}) + +#install(TARGETS minimal ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) + +kirigami_package_breeze_icons(ICONS open-menu-symbolic document-decrypt folder-sync go-next go-previous go-up handle-left handle-right view-list-icons applications-graphics media-record-symbolic) diff --git a/examples/staticcmake/src/Page1.qml b/examples/staticcmake/src/Page1.qml new file mode 100644 index 0000000..d77f87b --- /dev/null +++ b/examples/staticcmake/src/Page1.qml @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick + +Page1Form { + button1.onClicked: { + console.log("Button Pressed. Entered text: " + textField1.text); + } +} diff --git a/examples/staticcmake/src/Page1Form.ui.qml b/examples/staticcmake/src/Page1Form.ui.qml new file mode 100644 index 0000000..7f58e4d --- /dev/null +++ b/examples/staticcmake/src/Page1Form.ui.qml @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +Kirigami.Page { + title: qsTr("Page 1") + + property alias textField1: textField1 + property alias button1: button1 + + actions: [ + Kirigami.Action { + text: "Sync" + icon.name: "folder-sync" + onTriggered: showPassiveNotification("Action clicked") + } + ] + + RowLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.topMargin: 20 + anchors.top: parent.top + + TextField { + id: textField1 + placeholderText: qsTr("Text Field") + } + + Button { + id: button1 + text: qsTr("Press Me") + } + } +} diff --git a/examples/staticcmake/src/kirigami-icons.qrc b/examples/staticcmake/src/kirigami-icons.qrc new file mode 100644 index 0000000..19c7da7 --- /dev/null +++ b/examples/staticcmake/src/kirigami-icons.qrc @@ -0,0 +1,15 @@ + + + ../3rdparty/breeze-icons/icons/actions/24/open-menu-symbolic.svg + ../3rdparty/breeze-icons/icons/actions/32/document-decrypt.svg + ../3rdparty/breeze-icons/icons/actions/32/folder-sync.svg + ../3rdparty/breeze-icons/icons/actions/22/go-next.svg + ../3rdparty/breeze-icons/icons/actions/22/go-previous.svg + ../3rdparty/breeze-icons/icons/actions/22/go-up.svg + ../3rdparty/breeze-icons/icons/actions/22/handle-left.svg + ../3rdparty/breeze-icons/icons/actions/22/handle-right.svg + ../3rdparty/breeze-icons/icons/actions/32/view-list-icons.svg + ../3rdparty/breeze-icons/icons/categories/32/applications-graphics.svg + ../3rdparty/breeze-icons/icons/actions/symbolic/media-record-symbolic.svg + + diff --git a/examples/staticcmake/src/main.cpp b/examples/staticcmake/src/main.cpp new file mode 100644 index 0000000..0c26f66 --- /dev/null +++ b/examples/staticcmake/src/main.cpp @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifdef Q_OS_ANDROID +#include +#else +#include +#endif + +#include +#include +#include + +#ifdef Q_OS_ANDROID +#include + +// WindowManager.LayoutParams +#define FLAG_TRANSLUCENT_STATUS 0x04000000 +#define FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 0x80000000 +// View +#define SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 0x00002000 + +#endif + +Q_IMPORT_PLUGIN(KirigamiPlugin) + +Q_DECL_EXPORT int main(int argc, char *argv[]) +{ +// The desktop QQC2 style needs it to be a QApplication +#ifdef Q_OS_ANDROID + QGuiApplication app(argc, argv); +#else + QApplication app(argc, argv); +#endif + + // qputenv("QML_IMPORT_TRACE", "1"); + + QQmlApplicationEngine engine; + + engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); + + if (engine.rootObjects().isEmpty()) { + return -1; + } + + // HACK to color the system bar on Android, use qtandroidextras and call the appropriate Java methods +#ifdef Q_OS_ANDROID + QtAndroid::runOnAndroidThread([=]() { + QAndroidJniObject window = QtAndroid::androidActivity().callObjectMethod("getWindow", "()Landroid/view/Window;"); + window.callMethod("addFlags", "(I)V", FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.callMethod("clearFlags", "(I)V", FLAG_TRANSLUCENT_STATUS); + window.callMethod("setStatusBarColor", "(I)V", QColor("#2196f3").rgba()); + window.callMethod("setNavigationBarColor", "(I)V", QColor("#2196f3").rgba()); + }); +#endif + + return app.exec(); +} diff --git a/examples/staticcmake/src/main.qml b/examples/staticcmake/src/main.qml new file mode 100644 index 0000000..0593f82 --- /dev/null +++ b/examples/staticcmake/src/main.qml @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + visible: true + title: qsTr("Hello World") + + pageStack.initialPage: Page1 {} + + globalDrawer: Kirigami.GlobalDrawer { + actions: [ + Kirigami.Action { + text: "View" + icon.name: "view-list-icons" + Kirigami.Action { + text: "action 1" + } + Kirigami.Action { + text: "action 2" + } + Kirigami.Action { + text: "action 3" + } + }, + Kirigami.Action { + text: "action 3" + }, + Kirigami.Action { + text: "action 4" + } + ] + } +} diff --git a/examples/staticcmake/src/qtquickcontrols2.conf b/examples/staticcmake/src/qtquickcontrols2.conf new file mode 100644 index 0000000..c22fe2d --- /dev/null +++ b/examples/staticcmake/src/qtquickcontrols2.conf @@ -0,0 +1,15 @@ +; This file can be edited to change the style of the application +; See Styling Qt Quick Controls 2 in the documentation for details: +; http://doc.qt.io/qt-5/qtquickcontrols2-styles.html + +[Controls] +Style=Material + +[Universal] +Theme=Light +;Accent=Steel + +[Material] +Theme=Light +Accent=BlueGrey +Primary=BlueGray diff --git a/examples/staticcmake/src/resources.qrc b/examples/staticcmake/src/resources.qrc new file mode 100644 index 0000000..44587bd --- /dev/null +++ b/examples/staticcmake/src/resources.qrc @@ -0,0 +1,8 @@ + + + main.qml + Page1.qml + Page1Form.ui.qml + qtquickcontrols2.conf + + diff --git a/examples/wheelhandler/FlickableUsage.qml b/examples/wheelhandler/FlickableUsage.qml new file mode 100644 index 0000000..4144e4d --- /dev/null +++ b/examples/wheelhandler/FlickableUsage.qml @@ -0,0 +1,71 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +QQC2.ApplicationWindow { + id: root + + width: flickable.implicitWidth + height: flickable.implicitHeight + visible: true + + Flickable { + id: flickable + + anchors.fill: parent + implicitWidth: wheelHandler.horizontalStepSize * 10 + leftMargin + rightMargin + implicitHeight: wheelHandler.verticalStepSize * 10 + topMargin + bottomMargin + + leftMargin: QQC2.ScrollBar.vertical.visible && QQC2.ScrollBar.vertical.mirrored ? QQC2.ScrollBar.vertical.width : 0 + rightMargin: QQC2.ScrollBar.vertical.visible && !QQC2.ScrollBar.vertical.mirrored ? QQC2.ScrollBar.vertical.width : 0 + bottomMargin: QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.height : 0 + + contentWidth: contentItem.childrenRect.width + contentHeight: contentItem.childrenRect.height + + Kirigami.WheelHandler { + id: wheelHandler + target: flickable + filterMouseEvents: true + keyNavigationEnabled: true + } + + QQC2.ScrollBar.vertical: QQC2.ScrollBar { + parent: flickable.parent + height: flickable.height - flickable.topMargin - flickable.bottomMargin + x: mirrored ? 0 : flickable.width - width + y: flickable.topMargin + active: flickable.QQC2.ScrollBar.horizontal.active + stepSize: wheelHandler.verticalStepSize / flickable.contentHeight + } + + QQC2.ScrollBar.horizontal: QQC2.ScrollBar { + parent: flickable.parent + width: flickable.width - flickable.leftMargin - flickable.rightMargin + x: flickable.leftMargin + y: flickable.height - height + active: flickable.QQC2.ScrollBar.vertical.active + stepSize: wheelHandler.horizontalStepSize / flickable.contentWidth + } + + Grid { // Example content + columns: Math.sqrt(visibleChildren.length) + Repeater { + model: 1000 + delegate: Rectangle { + implicitWidth: wheelHandler.horizontalStepSize + implicitHeight: wheelHandler.verticalStepSize + gradient: Gradient { + orientation: index % 2 ? Gradient.Vertical : Gradient.Horizontal + GradientStop { position: 0; color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1) } + GradientStop { position: 1; color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1) } + } + } + } + } + } +} diff --git a/examples/wheelhandler/ScrollViewUsage.qml b/examples/wheelhandler/ScrollViewUsage.qml new file mode 100644 index 0000000..23edc50 --- /dev/null +++ b/examples/wheelhandler/ScrollViewUsage.qml @@ -0,0 +1,46 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +T.ScrollView { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + contentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + contentHeight + topPadding + bottomPadding) + + leftPadding: mirrored && T.ScrollBar.vertical.visible && !Kirigami.Settings.isMobile ? T.ScrollBar.vertical.width : 0 + rightPadding: !mirrored && T.ScrollBar.vertical.visible && !Kirigami.Settings.isMobile ? T.ScrollBar.vertical.width : 0 + bottomPadding: T.ScrollBar.horizontal.visible && !Kirigami.Settings.isMobile ? T.ScrollBar.horizontal.height : 0 + + data: [ + Kirigami.WheelHandler { + id: wheelHandler + target: control.contentItem + } + ] + + T.ScrollBar.vertical: QQC2.ScrollBar { + parent: control + x: control.mirrored ? 0 : control.width - width + y: control.topPadding + height: control.availableHeight + active: control.T.ScrollBar.horizontal.active + stepSize: wheelHandler.verticalStepSize / control.contentHeight + } + + T.ScrollBar.horizontal: QQC2.ScrollBar { + parent: control + x: control.leftPadding + y: control.height - height + width: control.availableWidth + active: control.T.ScrollBar.vertical.active + stepSize: wheelHandler.horizontalStepSize / control.contentWidth + } +} diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..a63448d Binary files /dev/null and b/logo.png differ diff --git a/metainfo.yaml b/metainfo.yaml new file mode 100644 index 0000000..86fe678 --- /dev/null +++ b/metainfo.yaml @@ -0,0 +1,21 @@ +description: QtQuick plugins to build user interfaces based on the KDE human interface guidelines +tier: 1 +type: functional +platforms: + - name: Linux + - name: FreeBSD + - name: Android + - name: iOS + - name: Windows + - name: macOS +public_lib: true +deprecated: false +release: true +logo: logo.png +libraries: + - cmake: KF6::Kirigami +cmakename: KF6Kirigami +irc: kde-kirigami +mailinglist: plasma-devel +group: Frameworks +subgroup: Tier 1 diff --git a/poqm/ar/libkirigami6_qt.po b/poqm/ar/libkirigami6_qt.po new file mode 100644 index 0000000..5015323 --- /dev/null +++ b/poqm/ar/libkirigami6_qt.po @@ -0,0 +1,336 @@ +# SPDX-FileCopyrightText: 2021, 2022, 2023, 2024, 2025 Zayed Al-Saidi +# Safa Alfulaij , 2017, 2018. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2025-03-13 11:48+0400\n" +"Last-Translator: Zayed Al-Saidi \n" +"Language-Team: ar\n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" +"X-Generator: Lokalize 23.08.5\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "‏%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "أرسِل بريد الإلكتروني إلى %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "شاركنا" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "تبرّع" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "أبلغ عن علّة" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "حقوق النسخ" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "الرّخصة:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "الترخيص: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "المكتبات المستخدمة" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "المؤلفين" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "أظهر صور المطور" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "إشادات" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "المترجمون" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "حول %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "أنهِ" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "إجراءات أخرى" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "أزل الوسم" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "إجراءات" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "اعرض المساعدة السياقية" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "عُد" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "أغلق الشريط الجانبي" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "افتح الشريط الجانبي" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "يحمّل..." + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "كلمة السّرّ" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "أخف كلمة السّرّ" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "أظهر كلمة السّرّ" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "أغلق القائمة" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "افتح القائمة" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "ابحث..." + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "ابحث" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "مسح سجل البحث" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "انسخ" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "حدّد الكلّ" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "اكتمل" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "تحذير" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "خطأ" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "ملاحظة" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "أغلق" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "أغلق الخزنة" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "افتح الخزنة" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "أغلق" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "تنقّل للخلف" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "تنقّل للأمام" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "افتح الوصلة %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "افتح الوصلة" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "انسخ الوصلة إلى الحافظة" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "أغلق" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "إصدارة إطار كِيدِي %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "نظام نوافذ %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "كيوت %2 (بني على %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "أغلق" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "إعدادات" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "إعدادات — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "الصفحة الحالية. التقدم: %1 بالمائة" + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "انتقل إلى %1. التقدم: %2 بالمائة" + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "الصّفحة الحاليّة" + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "انتقل إلى %1. تطلب اهتمام." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "تنقّل إلى %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "إجراءات أخرى" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "زر صفحة %1 في متجر كِيدِي" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "انسخ عنوان الوصلة" + +#, fuzzy +#~| msgctxt "AboutPage|" +#~| msgid "(%1)" +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#, fuzzy +#~| msgctxt "ContextDrawer|" +#~| msgid "Actions" +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "إجراءات" diff --git a/poqm/ast/libkirigami6_qt.po b/poqm/ast/libkirigami6_qt.po new file mode 100644 index 0000000..c0f7041 --- /dev/null +++ b/poqm/ast/libkirigami6_qt.po @@ -0,0 +1,287 @@ +# SPDX-FileCopyrightText: 2023 Enol P. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2023-12-14 23:02+0100\n" +"Last-Translator: Enol P. \n" +"Language-Team: Assamese \n" +"Language: ast\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 23.08.4\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Llicencia:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Llicencia: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Biblioteques n'usu" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Creitos" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Traductores" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Más aiciones" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Aiciones" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Atrás" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Zarrar la barra llateral" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Abrir la barra llateral" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Cargando…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Contraseña" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Buscar…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "" + +#: controls/SelectableLabel.qml:179 +#, fuzzy +#| msgctxt "AboutItem|" +#| msgid "Copyright" +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Copyright" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Zarrar el caxón" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Abrir el caxón" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "El sistema de ventanes %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Configuración" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Configuración — %1" + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Más aiciones" diff --git a/poqm/az/libkirigami6_qt.po b/poqm/az/libkirigami6_qt.po new file mode 100644 index 0000000..b2bead6 --- /dev/null +++ b/poqm/az/libkirigami6_qt.po @@ -0,0 +1,371 @@ +# Xəyyam , 2020, 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-07-20 08:58+0400\n" +"Last-Translator: Kheyyam \n" +"Language-Team: Azerbaijani \n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 22.04.3\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "%1 ünvanına e-poçt göndərin" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Iştirak edin" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "" + +#: controls/AboutItem.qml:245 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Report Bug…" +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Xəta hesabatı göndərin..." + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Müəllif hüquqları" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Lisenziya:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Lisenziya: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "İstifadə olunan kitabxana" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Müəlliflər" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Müəllifin fotoşəklləri" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Minnətdarlıq" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Tərcüməçilər" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1 haqqında" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Çıxış" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Daha Çox Fəaliyyətlər" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Yarlığı silin" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Əməllər" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Geriyə" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Yandakı paneli bağla" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Yan paneli açın" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Yüklənir..." + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Şifrə" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Şifrə" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Şifrə" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Bağlamaq" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Open" +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Açmaq" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Axtarış" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Axtarış" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "" + +#: controls/SelectableLabel.qml:179 +#, fuzzy +#| msgctxt "AboutItem|" +#| msgid "Copyright" +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Müəllif hüquqları" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Bağlamaq" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Çəkməcəni bağlayın" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Çəkməcəni açın" + +#: controls/templates/OverlaySheet.qml:290 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Bağlamaq" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Geriyə hərəkət" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "İrəli hərəkət" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Open" +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Açmaq" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Keçidi mübadilə yaddaşına kopyalayın" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Bağlamaq" + +#: platform/settings.cpp:219 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "KDE Frameworks %1" +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "The %1 windowing system" +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "%1 qrafik server platforması" + +#: platform/settings.cpp:222 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "Qt %2 (built against %3)" +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (%3 versiyadan yığılıb)" + +#, fuzzy +#~| msgctxt "OverlayDrawer|" +#~| msgid "Close" +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Bağlamaq" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Ayarlar" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Ayarlar — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Cari səhifə. Gedişat: %1 faiz." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "İstiqamət %1. Gedişat: %2 faiz." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Cari səhifə." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "İstiqamət %1. Diqqət tələb olunur." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "İstiqamət %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Bir çox işlər" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "%1 KDE Mağazası səhifəsini ziyarət edin" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Keçid ünvanını kopyalamaq" + +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Axtarış..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" diff --git a/poqm/bg/libkirigami6_qt.po b/poqm/bg/libkirigami6_qt.po new file mode 100644 index 0000000..5696baa --- /dev/null +++ b/poqm/bg/libkirigami6_qt.po @@ -0,0 +1,277 @@ +# SPDX-FileCopyrightText: 2022, 2023, 2024, 2025 Mincho Kondarev +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: 2025-03-22 16:53+0100\n" +"Last-Translator: Mincho Kondarev \n" +"Language-Team: Bulgarian \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"X-Generator: Lokalize 25.03.70\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Изпращане на имейл до %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Включете се" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Дарение" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Подаване на сигнал за грешка" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Авторско право" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Лиценз:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Лиценз: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Използвани библиотеки" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Автори" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Показване на снимки на автора" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Кредити" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Преводачи" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Относно %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Изход" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Допълнителни действия" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Премахване на етикет" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Действия" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Показване на контекстна помощ" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Назад" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Затваряне на страничната лента" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Отваряне на страничната лента" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Зареждане…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Парола" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Скриване на паролата" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Показване на паролата" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Затваряне на меню" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Отваряне на меню" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Търсене…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Търсене" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Изчистване на търсенето" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Копиране" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Избиране на всичко" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Успех" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Предупреждение" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Грешка" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Бележка" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Затваряне" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Затваряне на чекмеджето" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Отваряне на чекмеджето" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Затваряне" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Навигиране обратно" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Навигиране напред" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Отваряне на връзка %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Отваряне на връзка" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Копиране на връзката в клипборда" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Затваряне" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Система за прозорци %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (изграден върху %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Затваряне" diff --git a/poqm/ca/libkirigami6_qt.po b/poqm/ca/libkirigami6_qt.po new file mode 100644 index 0000000..613c6ea --- /dev/null +++ b/poqm/ca/libkirigami6_qt.po @@ -0,0 +1,286 @@ +# Translation of libkirigami6_qt.po to Catalan +# Copyright (C) 2016-2025 This_file_is_part_of_KDE +# This file is distributed under the license LGPL version 2.1 or +# version 3 or later versions approved by the membership of KDE e.V. +# +# SPDX-FileCopyrightText: 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024 Josep M. Ferrer +# Antoni Bella Pérez , 2016, 2020, 2021. +# Empar Montoro Martín , 2019. +# Josep M. Ferrer , 2025. +msgid "" +msgstr "" +"Project-Id-Version: kirigami\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"PO-Revision-Date: 2025-03-12 09:43+0100\n" +"Last-Translator: Josep M. Ferrer \n" +"Language-Team: Catalan \n" +"Language: ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Accelerator-Marker: &\n" +"X-Generator: Lokalize 22.12.3\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Envia un correu a %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Col·laboreu-hi" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Donatius" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Informeu d'un error" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Drets d'autor" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Llicència:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Llicència: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Biblioteques en ús" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autors" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Mostra les fotos dels autors" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Atribucions" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Traductors" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Quant al %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Surt" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Més accions" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Elimina l'etiqueta" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Accions" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Mostra l'ajuda contextual" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Enrere" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Tanca la barra lateral" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Obre la barra lateral" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "S'està carregant…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Contrasenya" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Oculta la contrasenya" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Mostra la contrasenya" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Tanca el menú" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Obre el menú" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Cerca…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Cerca" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Neteja la cerca" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Copia" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Selecciona-ho tot" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Correcte" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Avís" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Error" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Nota" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Tanca" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Tanca el calaix" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Obre el calaix" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Tanca" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navega enrere" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navega endavant" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Obre l'enllaç %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Obre l'enllaç" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copia l'enllaç al porta-retalls" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Tanca" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "Frameworks %1 de KDE" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "El sistema de finestres %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (construïdes amb %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Tanca" diff --git a/poqm/ca@valencia/libkirigami6_qt.po b/poqm/ca@valencia/libkirigami6_qt.po new file mode 100644 index 0000000..8ea69f4 --- /dev/null +++ b/poqm/ca@valencia/libkirigami6_qt.po @@ -0,0 +1,282 @@ +# Translation of libkirigami6_qt.po to Catalan (Valencian) +# Copyright (C) 2016-2025 This_file_is_part_of_KDE +# This file is distributed under the license LGPL version 2.1 or +# version 3 or later versions approved by the membership of KDE e.V. +# +# SPDX-FileCopyrightText: 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024 Josep M. Ferrer +# Antoni Bella Pérez , 2016, 2020, 2021. +# Empar Montoro Martín , 2019. +# Josep M. Ferrer , 2025. +msgid "" +msgstr "" +"Project-Id-Version: kirigami\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"PO-Revision-Date: 2025-03-12 09:43+0100\n" +"Last-Translator: Josep M. Ferrer \n" +"Language-Team: Catalan \n" +"Language: ca@valencia\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Accelerator-Marker: &\n" +"X-Generator: Lokalize 22.12.3\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Envia un correu a %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Col·laboreu-hi" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Donatius" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Informeu d'un error" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Drets d'autoria" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Llicència:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Llicència: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Biblioteques en ús" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autoria" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Mostra les fotos dels autors" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Atribucions" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Equip de traducció" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Quant a %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Ix" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Més accions" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Elimina l'etiqueta" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Accions" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Mostra l'ajuda contextual" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Arrere" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Tanca la barra lateral" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Obri la barra lateral" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "S'està carregant…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Contrasenya" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Oculta la contrasenya" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Mostra la contrasenya" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Tanca el menú" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Obri el menú" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Busca…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Busca" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Neteja la busca" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Copia" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Selecciona-ho tot" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Correcte" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Avís" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "S'ha produït un error" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Nota" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Tanca" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Tanca el calaix" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Obri el calaix" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Tanca" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navega arrere" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navega avant" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Obri l'enllaç %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Obri l'enllaç" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copia l'enllaç a dins del porta-retalls" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Tanca" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "Frameworks %1 de KDE" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "El sistema de finestres %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (construïdes amb %3)" diff --git a/poqm/cs/libkirigami6_qt.po b/poqm/cs/libkirigami6_qt.po new file mode 100644 index 0000000..5fac946 --- /dev/null +++ b/poqm/cs/libkirigami6_qt.po @@ -0,0 +1,280 @@ +# SPDX-FileCopyrightText: 2016, 2017, 2018, 2019, 2020, 2023, 2024 Vít Pelčák +# SPDX-FileCopyrightText: 2021, 2022, 2023, 2024 Vit Pelcak +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2024-10-18 11:29+0200\n" +"Last-Translator: Vit Pelcak \n" +"Language-Team: Czech \n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"X-Generator: Lokalize 24.08.2\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Odeslat e-mail na %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Zapojte se" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Přispět" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Nahlásit chybu" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Autorská práva" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licence:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licence: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Použité knihovny" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autoři" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Zobrazit fotografie autora" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Poděkování" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Překladatelé" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "O aplikaci %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Ukončit" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Další činnosti" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Odstranit značku" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Činnosti" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Zobrazit kontextovou nápovědu" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Zpět" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Zavřít postranní panel" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Otevřít postranní panel" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Probíhá načítání…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Heslo" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Skrýt heslo" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Zobrazit heslo" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Uzavřít nabídku" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Otevřít nabídku" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Hledat…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Hledat" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Vyprázdnit hledání" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Kopírovat" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Vybrat vše" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Zavřít šuplík" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Otevřít šuplík" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Zavřít" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Přejít zpět" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Přejít vpřed" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Otevřít odkaz %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Otevřít odkaz" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Kopírovat odkaz do schránky" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "OverlaySheet|@action:button close dialog" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Zavřít" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Okenní systém %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (sestaveno oproti %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Zavřít" diff --git a/poqm/da/libkirigami6_qt.po b/poqm/da/libkirigami6_qt.po new file mode 100644 index 0000000..edb84a9 --- /dev/null +++ b/poqm/da/libkirigami6_qt.po @@ -0,0 +1,310 @@ +# Martin Schlander , 2017, 2018, 2019, 2020. +# SPDX-FileCopyrightText: 2024 rasmus rosendahl-kaa +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2024-10-21 12:27+0200\n" +"Last-Translator: rasmus rosendahl-kaa \n" +"Language-Team: Danish \n" +"Language: da\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 24.08.2\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Send en e-mail til %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Bliv involveret" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Donér" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Rapportér en fejl" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Ophavsret" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licens:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licens: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Biblioteker i brug" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Ophavsmænd" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Vis fotos af ophavsmænd" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Bidragsydere" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Oversættere" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Om %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Afslut" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Flere handlinger" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Fjern mærke" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Handlinger" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Vis kontekstbaseret hjælp" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Tilbage" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Luk sidepanel" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Åbn sidepanel" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Indlæser…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Adgangskode" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Skjul adgangskode" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Vis adgangskode" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Luk menu" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Åbn menu" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Søg…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Søg" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Ryd søgning" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Kopiér" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Vælg alle" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +#, fuzzy +#| msgctxt "OverlaySheet|@action:button close dialog" +#| msgid "Close" +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Luk" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Luk skuffe" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Åbn skuffe" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Luk" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navigér tilbage" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navigér fremad" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Åbn link %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Åbn link" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Kopiér link til udklipsholderen" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "OverlaySheet|@action:button close dialog" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Luk" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Vinduessystemet %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (bygget op imod %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Luk" + +#, fuzzy +#~| msgctxt "ForwardButton|" +#~| msgid "Navigate Forward" +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Navigér fremad" + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Flere handlinger" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Kopiér linkadresse" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Søg..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Flere handlinger" diff --git a/poqm/de/libkirigami6_qt.po b/poqm/de/libkirigami6_qt.po new file mode 100644 index 0000000..b9abcda --- /dev/null +++ b/poqm/de/libkirigami6_qt.po @@ -0,0 +1,345 @@ +# SPDX-FileCopyrightText: 2024 Johannes Obermayr +# Frederik Schwarzer , 2016, 2018, 2020, 2021, 2022, 2023. +# Burkhard Lück , 2017, 2018, 2019, 2020, 2021. +# Alois Spitzbart , 2021. +msgid "" +msgstr "" +"Project-Id-Version: libkirigami6_qt\n" +"PO-Revision-Date: 2024-12-23 19:07+0100\n" +"Last-Translator: Johannes Obermayr \n" +"Language-Team: German \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 25.03.70\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Eine E-Mail an %1 senden" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Machen Sie mit" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Spenden" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Probleme oder Wünsche berichten" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Lizenz:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Lizenz: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Verwendete Bibliotheken" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autoren" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Fotos von Autoren anzeigen" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Danksagungen" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Übersetzer" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Über %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Beenden" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Weitere Aktionen" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Stichwort entfernen" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Aktionen" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Kontexthilfe anzeigen" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Zurück" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Seitenleiste schließen" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Seitenleiste öffnen" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Wird geladen ..." + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Passwort" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Passwort ausblenden" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Passwort anzeigen" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Menü schließen" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Menü öffnen" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Suchen ..." + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Suchen" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Suche leeren" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Kopieren" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Alles auswählen" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Erfolg" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Warnung" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Fehler" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Anmerkung" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Schließen" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Seitenleiste schließen" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Seitenleiste öffnen" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Schließen" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Zurück gehen" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Vorwärts gehen" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Verknüpfung %1 öffnen" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Verknüpfung öffnen" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Verknüpfung in die Zwischenablage kopieren" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "InlineMessage|" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Schließen" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Das Fenstersystem %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (kompiliert gegen %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Schließen" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Einstellungen" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Einstellungen — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Aktuelle Seite. Fortschritt: %1 Prozent." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Zu %1 gehen. Fortschritt: %2 Prozent." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Aktuelle Seite." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Zu %1 gehen. Erfordert Aufmerksamkeit." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Zu %1 gehen." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Weitere Aktionen" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Besuchen Sie %1 im KDE Store" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Verknüpfungsadresse kopieren" + +#, fuzzy +#~| msgctxt "AboutPage|" +#~| msgid "(%1)" +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Suchen ..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Weitere Aktionen" diff --git a/poqm/el/libkirigami6_qt.po b/poqm/el/libkirigami6_qt.po new file mode 100644 index 0000000..7563fe5 --- /dev/null +++ b/poqm/el/libkirigami6_qt.po @@ -0,0 +1,365 @@ +# Stelios , 2017, 2020, 2021. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2021-10-06 10:25+0300\n" +"Last-Translator: Stelios \n" +"Language-Team: Greek \n" +"Language: el\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 20.04.2\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Αποστολή μηνύματος στο %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Ασχοληθείτε" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "" + +#: controls/AboutItem.qml:245 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Report Bug…" +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Αναφορά σφάλματος…" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Άδεια χρήσης:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Άδεια χρήσης: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Βιβλιοθήκες σε χρήση" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Συγγραφείς" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Εμφάνιση φωτογραφιών συγγραφέων" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Ευχαριστίες" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Μεταφραστές" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Περίγραμμα %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Έξοδος" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Περισσότερες ενέργειες" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Ενέργειες" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Πίσω" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Κλείσιμο πλευρικής γραμμής" + +#: controls/GlobalDrawer.qml:666 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Κλείσιμο πλευρικής γραμμής" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Κωδικός πρόσβασης" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Κωδικός πρόσβασης" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Κωδικός πρόσβασης" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Κλείσιμο" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Open" +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Άνοιγμα" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Αναζήτηση…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Αναζήτηση" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "" + +#: controls/SelectableLabel.qml:179 +#, fuzzy +#| msgctxt "AboutItem|" +#| msgid "Copyright" +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Copyright" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Κλείσιμο" + +#: controls/templates/OverlayDrawer.qml:128 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Κλείσιμο πλευρικής γραμμής" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Κλείσιμο" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Πλοήγηση προς τα πίσω" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Πλοήγηση προς τα εμπρός" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Open" +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Άνοιγμα" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "InlineMessage|" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Κλείσιμο" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Το %1 παραθυρικό σύστημα" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (κατασκευάστηκε με βάση το %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Κλείσιμο" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Ρυθμίσεις" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Ρυθμίσεις — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Τρέχουσα σελίδα. Πρόοδος: %1 τοις εκατό." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Πλοήγηση προς το %1. Πρόοδος: %2 τοις εκατό." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Τρέχουσα σελίδα." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Πλοήγηση προς το %1. Δώστε προσοχή." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Πλοήγηση προς το %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Περισσότερες ενέργειες" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Επισκεφθείτε τη σελίδα της αποθήκης του KDE για το %1" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Αντιγραφή διεύθυνσης δεσμού" + +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Αναζήτηση..." + +#, fuzzy +#~| msgctxt "ContextDrawer|" +#~| msgid "Actions" +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Ενέργειες" diff --git a/poqm/en_GB/libkirigami6_qt.po b/poqm/en_GB/libkirigami6_qt.po new file mode 100644 index 0000000..1ff7e34 --- /dev/null +++ b/poqm/en_GB/libkirigami6_qt.po @@ -0,0 +1,342 @@ +# SPDX-FileCopyrightText: 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024 Steve Allewell +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2024-10-18 17:12+0100\n" +"Last-Translator: Steve Allewell \n" +"Language-Team: British English\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 24.08.2\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Send an email to %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Get Involved" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Donate" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Report a Bug" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licence:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licence: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Libraries in use" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Authors" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Show author photos" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Credits" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Translators" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "About %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Quit" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "More Actions" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Remove Tag" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Actions" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Show Contextual Help" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Back" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Close Sidebar" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Open Sidebar" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Loading…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Password" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Hide Password" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Show Password" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Close menu" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Open menu" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Search…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Search" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Clear search" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Copy" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Select All" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +#, fuzzy +#| msgctxt "OverlaySheet|@action:button close dialog" +#| msgid "Close" +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Close" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Close drawer" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Open drawer" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Close" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navigate Back" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navigate Forward" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Open link %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Open link" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copy Link to Clipboard" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "OverlaySheet|@action:button close dialog" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Close" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "The %1 windowing system" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (built against %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Close" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Settings" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Settings — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Current page. Progress: %1 percent." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Navigate to %1. Progress: %2 percent." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Current page." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Navigate to %1. Demanding attention." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Navigate to %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "More Actions" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Visit %1's KDE Store page" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Copy link address" + +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Search..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "More Actions" diff --git a/poqm/eo/libkirigami6_qt.po b/poqm/eo/libkirigami6_qt.po new file mode 100644 index 0000000..f43e80e --- /dev/null +++ b/poqm/eo/libkirigami6_qt.po @@ -0,0 +1,278 @@ +# translation of libkirigami6_qt.pot to Esperanto +# Copyright (C) 2016 Free Software Foundation, Inc. +# This file is distributed under the same license as the kirigami package. +# Oliver Kellogg , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: kirigami\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2016-12-21 04:03+0100\n" +"PO-Revision-Date: 2025-03-15 10:38+0100\n" +"Last-Translator: Oliver Kellogg \n" +"Language-Team: Esperanto \n" +"Language: eo\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"X-Generator: translate-po (https://github.com/zcribe/translate-po)\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Sendi retmesaĝon al %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Implikiĝi" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Doni" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Raporti Cimon" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Kopirajto" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Permesilo:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Permesilo: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Bibliotekoj en uzo" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Aŭtoroj" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Montri aŭtorajn fotojn" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Kreditoj" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Tradukistoj" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Pri %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Forlasi" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Pli da Agoj" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Forigi Etikedon" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Agoj" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Montri Kuntekstan Helpon" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Reen" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Fermi Flankbreton" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Malfermi Flankbreton" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Ŝargante…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Pasvorto" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Kaŝi Pasvorton" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Malkaŝi Pasvorton" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Fermi menuon" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Malfermi menuon" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Serĉi" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Serĉi" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Klara serĉo" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Kopii" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Elekti Ĉiujn" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Sukceso" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Averto" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Eraro" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Noto" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Fermi" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Fermi tirkeston" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Malfermi tirkeston" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Fermi" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navigi Reen" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navigi Antaŭen" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Malfermi ligilon %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Malfermi ligilon" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Kopii Ligilon al Tondujo" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Fermi" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "La %1 fenestra sistemo" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (konstruita kontraŭ %3)" diff --git a/poqm/es/libkirigami6_qt.po b/poqm/es/libkirigami6_qt.po new file mode 100644 index 0000000..2c0f2f6 --- /dev/null +++ b/poqm/es/libkirigami6_qt.po @@ -0,0 +1,275 @@ +# Spanish translations for libkirigami6_qt.po package. +# +# SPDX-FileCopyrightText: 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025 Eloy Cuadra +msgid "" +msgstr "" +"Project-Id-Version: libkirigami6_qt\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"PO-Revision-Date: 2025-03-13 01:05+0100\n" +"Last-Translator: Eloy Cuadra \n" +"Language-Team: Spanish \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 24.12.3\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Enviar un correo electrónico a %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Involucrarse" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Hacer un donativo" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Informar de fallo" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licencia:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licencia: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Bibliotecas en uso" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autores" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Mostrar fotos del autor" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Créditos" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Traductores" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Acerca de %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Salir" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Más acciones" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Eliminar etiqueta" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Acciones" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Mostrar ayuda de contexto" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Atrás" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Cerrar la barra lateral" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Abrir la barra lateral" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Cargando…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Contraseña" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Ocultar contraseña" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Mostrar contraseña" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Cerrar menú" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Abrir menú" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Buscar…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Buscar" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Borrar la búsqueda" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Copiar" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Seleccionar todo" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Éxito" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Advertencia" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Error" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Nota" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Cerrar" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Cerrar cajón" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Abrir cajón" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Cerrar" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Retroceder una página" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Avanzar una página" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Abrir enlace %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Abrir enlace" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copiar enlace en el portapapeles" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Cerrar" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "El sistema de ventanas %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (compilado para %3)" diff --git a/poqm/et/libkirigami6_qt.po b/poqm/et/libkirigami6_qt.po new file mode 100644 index 0000000..83bc7f7 --- /dev/null +++ b/poqm/et/libkirigami6_qt.po @@ -0,0 +1,363 @@ +# Marek Laane , 2019, 2020. +# Mihkel Tõnnov , 2020. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2020-10-07 19:58+0200\n" +"Last-Translator: Mihkel Tõnnov \n" +"Language-Team: Estonian <>\n" +"Language: et\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 20.08.1\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:172 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "Send an email to %1" +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Saada e-kiri aadressile %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "" + +#: controls/AboutItem.qml:258 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Autoriõigus" + +#: controls/AboutItem.qml:302 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "License:" +msgctxt "AboutItem|" +msgid "License:" +msgstr "Litsents:" + +#: controls/AboutItem.qml:324 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "License: %1" +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Litsents: %1" + +#: controls/AboutItem.qml:335 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Libraries in use" +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Kasutatavad teegid" + +#: controls/AboutItem.qml:365 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Authors" +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autorid" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:386 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Credits" +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Tunnustus" + +#: controls/AboutItem.qml:398 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Translators" +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Tõlkijad" + +#: controls/AboutPage.qml:100 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "About" +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Teave" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Veel toiminguid" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Toimingud" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Tagasi" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Sulge külgriba" + +#: controls/GlobalDrawer.qml:666 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Sulge külgriba" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Parool" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Parool" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Parool" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Sulge külgriba" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "" + +#: controls/SearchField.qml:86 +#, fuzzy +#| msgctxt "SearchField|" +#| msgid "Search" +msgctxt "SearchField|" +msgid "Search…" +msgstr "Otsi" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Otsi" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "" + +#: controls/SelectableLabel.qml:179 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Autoriõigus" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Sulge külgriba" + +#: controls/templates/OverlayDrawer.qml:128 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Sulge külgriba" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/OverlaySheet.qml:290 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Sulge külgriba" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Liigu tagasi" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Liigu edasi" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Sulge külgriba" + +#: platform/settings.cpp:219 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "KDE Frameworks %1" +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "The %1 windowing system" +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "%1 aknahaldur" + +#: platform/settings.cpp:222 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "Qt %2 (built against %3)" +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (ehitatud %3 peale)" + +#, fuzzy +#~| msgctxt "GlobalDrawer|" +#~| msgid "Close Sidebar" +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Sulge külgriba" + +#, fuzzy +#~| msgctxt "ForwardButton|" +#~| msgid "Navigate Forward" +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Liigu edasi" + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Veel toiminguid" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Kopeeri lingi aadress" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Otsi ..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" diff --git a/poqm/eu/libkirigami6_qt.po b/poqm/eu/libkirigami6_qt.po new file mode 100644 index 0000000..557072a --- /dev/null +++ b/poqm/eu/libkirigami6_qt.po @@ -0,0 +1,345 @@ +# Translation for libkirigami6_qt.po to Euskara/Basque (eu). +# Copyright (C) 2017-2025 This file is copyright: +# This file is distributed under the same license as the original file. +# SPDX-FileCopyrightText: 2023, 2024, 2025 KDE euskaratzeko proiektuko arduraduna +# +# Translators: +# Iñigo Salvador Azurmendi , 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025. +msgid "" +msgstr "" +"Project-Id-Version: kirigami\n" +"PO-Revision-Date: 2025-03-25 18:01+0100\n" +"Last-Translator: Iñigo Salvador Azurmendi \n" +"Language-Team: Basque \n" +"Language: eu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 24.12.3\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Bidali e-posta honi: %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Engaia zaitez" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Egin dohaintza" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Akats baten berri ematea" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Lizentzia:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Lizentzia: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Erabiltzen ari diren liburutegiak" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Egileak" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Erakutsi egilearen argazkiak" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Merituak" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Itzultzaileak" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1(e)ri buruz" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Irten" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Ekintza gehiago" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Kendu etiketa" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Ekintzak" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Erakutsi testuinguru laguntza" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Atzera" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Itxi alboko barra" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Itxi alboko barra" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Zamatzen…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Pasahitza" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Ezkutatu pasahitza" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Erakutsi pasahitza" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Itxi menua" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Ireki menua" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Bilatu..." + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Bilatu..." + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Garbitu bilaketa" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Kopiatu" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Hautatu dena" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Arrakasta" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Abisua" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Errorea" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Oharra" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Itxi" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Itxi tiradera" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Ireki tiradera" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Itxi" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Nabigatu atzera" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Nabigatu aurrera" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Iriki esteka, %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Ireki esteka" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Kopiatu esteka arbelera" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Itxi" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "%1 leiho-sistema" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (%3 erabiliz eraikia)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Itxi" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Ezarpenak" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Ezarpenak — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Uneko orria. Aurrerapena: ehuneko %1." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Nabigatu %1(e)ra. Aurrerapena: ehuneko %2." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Uneko orria." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Nabigatu %1(e)ra. Arreta eskatuz." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Nabigatu %1(e)ra." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Ekintza gehiago" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Bisitatu %1(r)(e)n KDE Biltegiko orria" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Kopiatu estekaren helbidea" + +#, fuzzy +#~| msgctxt "AboutPage|" +#~| msgid "(%1)" +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Bilatu..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Ekintza gehiago" diff --git a/poqm/fi/libkirigami6_qt.po b/poqm/fi/libkirigami6_qt.po new file mode 100644 index 0000000..8758c98 --- /dev/null +++ b/poqm/fi/libkirigami6_qt.po @@ -0,0 +1,339 @@ +# SPDX-FileCopyrightText: 2018, 2019, 2020, 2021, 2022, 2023, 2024 Tommi Nieminen +# Lasse Liehu , 2017. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2024-10-21 16:38+0300\n" +"Last-Translator: Tommi Nieminen \n" +"Language-Team: Finnish \n" +"Language: fi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 23.08.5\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Lähetä sähköpostia: %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Ota osaa" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Lahjoita" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Ilmoita viasta" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Tekijänoikeudet" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Lisenssi:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Lisenssi: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Käytetyt kirjastot" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Tekijät" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Näytä tekijöiden valokuvat" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Kiitokset" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Kääntäjät" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Tietoa – %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Lopeta" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Lisää toimintoja" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Poista luokitus" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Toiminnot" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Näytä kontekstiohje" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Takaisin" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Sulje sivupaneeli" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Avaa sivupaneeli" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Ladataan…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Salasana" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Piilota salasana" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Näytä salasana" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Sulje valikko" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Avaa valikko" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Etsi…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Etsi" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Tyhjennä haku" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Kopioi" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Valitse kaikki" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +#, fuzzy +#| msgctxt "OverlaySheet|@action:button close dialog" +#| msgid "Close" +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Sulje" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Sulje laatikko" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Avaa laatikko" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Sulje" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Siirry taaksepäin" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Siirry eteenpäin" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Avaa linkki %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Avaa linkki" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Kopioi linkki leikepöydälle" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "OverlaySheet|@action:button close dialog" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Sulje" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "%1 -ikkunointijärjestelmä" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (koostettu kirjastolla %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Sulje" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Asetukset" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Asetukset – %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Nykyinen sivu. Edistyminen: %1 %." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Siirry kohteeseen %1. Edistyminen: %2 %." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Nykyinen sivu." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Siirry kohteeseen %1. Vaatii huomiota." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Siirry kohteeseen %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Lisää toimintoja" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Käy KDE Storen %1 -sivulla" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Kopioi linkin osoite" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Etsi…" + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Lisää toimintoja" diff --git a/poqm/fr/libkirigami6_qt.po b/poqm/fr/libkirigami6_qt.po new file mode 100644 index 0000000..5cec4a7 --- /dev/null +++ b/poqm/fr/libkirigami6_qt.po @@ -0,0 +1,345 @@ +# SPDX-FileCopyrightText: 2020, 2021, 2022, 2023, 2024, 2025 Xavier Besnard +# Vincent Pinon , 2016, 2017. +# Simon Depiets , 2018, 2019. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2025-03-13 07:21+0100\n" +"Last-Translator: Xavier Besnard \n" +"Language-Team: French >\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Accelerator-Marker: &\n" +"X-Environment: kde\n" +"X-Generator: Lokalize 24.12.3\n" +"X-Qt-Contexts: true\n" +"X-Text-Markup: qtrich\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Envoyer un courriel à %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Impliquez vous" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Faire un don" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Signaler un bogue" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Droit d'auteur" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licence :" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licence : %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Bibliothèques en cours d'utilisation" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Auteurs" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Afficher les photos des auteurs" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Crédits" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Traducteurs" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "À propos de %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Quitter" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Plus d'actions" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Supprimer une étiquette" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Actions" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Afficher l'aide contextuelle" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Retour" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Fermer la barre latérale" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Ouvrir une barre latérale" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Chargement en cours..." + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Mot de passe" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Masquer un mot de passe" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Afficher un mot de passe" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Fermer le menu" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Ouvrir le menu" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Rechercher…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Rechercher" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Effacer une recherche" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Copier" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Tout sélectionner" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Réussite" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Avertissement" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Erreur" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Remarque" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Fermer" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Fermer un tiroir" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Ouvrir un tiroir" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Fermer" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navigation arrière" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navigation avant" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Ouvrir le lien %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Ouvrir un lien" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copier un lien dans le presse-papier" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Fermer" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "Environnements de développement %1 de KDE" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Le système de fenêtrage %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (Compilé avec %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Fermer" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Configuration" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Configuration — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Page actuelle. Avancement : %1 pourcent." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Naviguer vers %1. Avancement : %2 pourcent." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Page actuelle." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Naviguer vers %1. Attention requise." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Navigation vers %1" + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Actions supplémentaires" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Visiter la page de la boutique de KDE pour %1" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Copier l'adresse du lien" + +#~ msgctxt "LoadingPlaceholder|" +#~ msgid "Still loading, please wait." +#~ msgstr "Encore en cours de téléchargement, veuillez patienter." + +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Chercher…" + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Actions supplémentaires" diff --git a/poqm/gl/libkirigami6_qt.po b/poqm/gl/libkirigami6_qt.po new file mode 100644 index 0000000..dad7e0a --- /dev/null +++ b/poqm/gl/libkirigami6_qt.po @@ -0,0 +1,329 @@ +# SPDX-FileCopyrightText: 2023, 2024, 2025 Adrián Chaves (Gallaecio) +# Adrián Chaves Fernández (Gallaecio) , 2017. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2025-03-19 13:12+0100\n" +"Last-Translator: Adrián Chaves (Gallaecio) \n" +"Language-Team: Proxecto Trasno (proxecto@trasno.gal)\n" +"Language: gl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 24.12.3\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Enviar unha mensaxe de correo electrónico a %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Involucrarse" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Doar" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Informar dun fallo" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Dereitos de copia" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licenza:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licenza: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Bibliotecas usadas" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autoría" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Amosar fotos das persoas autoras" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Recoñecementos" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Tradución" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Sobre %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Saír" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Máis accións" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Retirar a etiqueta" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Accións" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Amosar a axuda contextual" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Atrás" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Pechar a barra lateral" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Abrir a barra lateral" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Cargando…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Contrasinal" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Agochar o contrasinal" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Amosar o contrasinal" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Pechar o menú" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Abre o menú" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Buscar…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Buscar" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Borrar a busca" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Copiar" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Seleccionar todo" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Éxito" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Aviso" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Erro" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Nota" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Pechar" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Pechar o caixón" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Abrir o caixón" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Pechar" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Volver" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Continuar" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Abrir a ligazón %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Abrir a ligazón." + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copiar a ligazón no portapapeis" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Pechar" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "Versión %1 das infraestruturas de KDE" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "O sistema de xanelas %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (construído sobre %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Pechar" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Configuración" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Configuración — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Páxina actual. Progreso: %1 por cento." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Navegar a %1. Progreso: %2 por cento." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Páxina actual." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Navegar a %1. Require atención." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Navegar a %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Máis accións" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Copiar o enderezo da ligazón" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Buscar…" + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Máis accións" diff --git a/poqm/he/libkirigami6_qt.po b/poqm/he/libkirigami6_qt.po new file mode 100644 index 0000000..7f15c6e --- /dev/null +++ b/poqm/he/libkirigami6_qt.po @@ -0,0 +1,277 @@ +# SPDX-FileCopyrightText: 2024, 2025 Yaron Shahrabani +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2025-03-12 07:39+0200\n" +"Last-Translator: Yaron Shahrabani \n" +"Language-Team: צוות התרגום של KDE ישראל\n" +"Language: he\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && " +"n % 10 == 0) ? 2 : 3));\n" +"X-Generator: Lokalize 24.12.2\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "שליחת דוא״ל אל %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "הצטרפות" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "תרומה" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "דיווח על תקלה" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "זכויות יוצרים" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "רישיון:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "רישיון: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "ספריות בשימוש" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "יוצרים" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "הצגת תמונות היוצרים" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "תודות" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "מתרגמים" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "על %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "יציאה" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "פעולות נוספות" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "הסרת תגית" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "פעולות" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "הצגת עזרה בהקשר" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "חזרה" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "סגירת סרגל צד" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "פתיחת סרגל צד" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "בטעינה…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "סיסמה" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "הסתרת סיסמה" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "הצגת סיסמה" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "סגירת התפריט" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "פתיחת התפריט" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "חיפוש…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "חיפוש" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "פינוי החיפוש" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "העתקה" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "בחירה בהכול" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "הצלחה" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "אזהרה" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "שגיאה" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "הערה" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "סגירה" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "סגירת המגירה" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "פתיחת המגירה" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "סגירה" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "ניווט אחורה" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "ניווט קדימה" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "פתיחת הקישור %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "פתיחת קישור" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "העתקת הקישור ללוח הגזירים" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "סגירה" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "תשתיות KDE‏ %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "מערכת החלונות %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (נבנה כנגד %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "סגירה" diff --git a/poqm/hi/libkirigami6_qt.po b/poqm/hi/libkirigami6_qt.po new file mode 100644 index 0000000..2b98f75 --- /dev/null +++ b/poqm/hi/libkirigami6_qt.po @@ -0,0 +1,403 @@ +# Raghavendra Kamath , 2021. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2021-08-21 10:53+0530\n" +"Last-Translator: Raghavendra Kamath \n" +"Language-Team: kde-hindi\n" +"Language: hi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=(n!=1);\n" +"X-Generator: Lokalize 21.08.0\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:172 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "Send an email to %1" +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "%1 को ईमेल भेजें" + +#: controls/AboutItem.qml:222 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Get Involved" +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "सहभाग लें" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "" + +#: controls/AboutItem.qml:258 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "सर्वाधिकार" + +#: controls/AboutItem.qml:302 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "License:" +msgctxt "AboutItem|" +msgid "License:" +msgstr "अनुज्ञापत्र :" + +#: controls/AboutItem.qml:324 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "License: %1" +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "अनुज्ञापत्र : %1" + +#: controls/AboutItem.qml:335 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Libraries in use" +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "उपयोग किए गए संग्रह" + +#: controls/AboutItem.qml:365 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Authors" +msgctxt "AboutItem|" +msgid "Authors" +msgstr "लेखक" + +#: controls/AboutItem.qml:375 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Show author photos" +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "लेखक की फोटोओं को दिखाएँ" + +#: controls/AboutItem.qml:386 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Credits" +msgctxt "AboutItem|" +msgid "Credits" +msgstr "आभार सूची" + +#: controls/AboutItem.qml:398 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Translators" +msgctxt "AboutItem|" +msgid "Translators" +msgstr "अनुवादक" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1 के बारे में" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "बाहर जाएँ" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "अधिक क्रियाएँ" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "क्रियाएं" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "पीछे" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "बाजूपट्टी बंद करें" + +#: controls/GlobalDrawer.qml:666 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "बाजूपट्टी बंद करें" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "कूटशब्द" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "कूटशब्द" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "कूटशब्द" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "बंद करें" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Open" +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "खोलें" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "खोजें…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "खोजें" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "" + +#: controls/SelectableLabel.qml:179 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "सर्वाधिकार" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "InlineMessage|" +msgid "Close" +msgstr "बंद करें" + +#: controls/templates/OverlayDrawer.qml:128 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "बाजूपट्टी बंद करें" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/OverlaySheet.qml:290 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "बंद करें" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "पीछे जाएँ" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "आगे जाएँ" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Open" +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "खोलें" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "बंद करें" + +#: platform/settings.cpp:219 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "KDE Frameworks %1" +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "केडीई फ्रेमवर्कस %1" + +#: platform/settings.cpp:221 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "The %1 windowing system" +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "%1 विंडो प्रणाली" + +#: platform/settings.cpp:222 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "Qt %2 (built against %3)" +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "क्यूट %2 (%3 के प्रती निर्मित)" + +#, fuzzy +#~| msgctxt "OverlayDrawer|" +#~| msgid "Close" +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "बंद करें" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "विन्यास" + +#, fuzzy +#~| msgctxt "CategorizedSettings|" +#~| msgid "Settings" +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "विन्यास" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "वर्तमान पृष्ठ। प्रगती : %1 प्रतिशत।" + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "%1 पर जाएँ। प्रगती : %2 प्रतिशत।" + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "वर्तमान पृष्ठ।" + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "%1 पर जाएँ। ध्यान देने की मांग कर रहा है।" + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "%1 पर जाएँ।" + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "अधिक क्रियाएँ" + +#, fuzzy +#~| msgctxt "AboutPage|" +#~| msgid "Visit %1's KDE Store page" +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "%1 के केडीई स्टोर पृष्ठ पर जाएँ" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "लिंक पता नक़ल करें" + +#, fuzzy +#~| msgctxt "AboutPage|" +#~| msgid "(%1)" +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" diff --git a/poqm/hu/libkirigami6_qt.po b/poqm/hu/libkirigami6_qt.po new file mode 100644 index 0000000..b351d26 --- /dev/null +++ b/poqm/hu/libkirigami6_qt.po @@ -0,0 +1,336 @@ +# Kiszel Kristóf , 2017, 2018, 2020, 2021. +# SPDX-FileCopyrightText: 2021, 2022, 2024, 2025 Kristof Kiszel +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2025-01-30 14:46+0100\n" +"Last-Translator: Kristof Kiszel \n" +"Language-Team: Hungarian \n" +"Language: hu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 24.12.1\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "E-mail küldése neki: %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Közreműködés" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Támogatás" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Hibabejelentés" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licenc:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licenc: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Felhasznált függvénykönyvtárak" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Szerzők" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "A szerző fényképének megjelenítése" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Köszönetnyilvánítás" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Fordítók" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Névjegy: %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Kilépés" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "További műveletek" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Címke eltávolítása" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Műveletek" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Környezetfüggő súgó megjelenítése" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Vissza" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Oldalsáv bezárása" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Oldalsáv megnyitása" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Betöltés…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Jelszó" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Jelszó elrejtése" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Jelszó megjelenítése" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Menü bezárása" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Menü megnyitása" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Keresés…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Keresés" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Keresés törlése" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Másolás" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Összes kijelölése" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Sikeres" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Figyelmeztetés" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Hiba" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Megjegyzés" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Bezárás" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Csúszómenü bezárása" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Csúszómenü megnyitása" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Bezárás" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Vissza" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Előre" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Hivatkozás megnyitása: %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Hivatkozás megnyitása" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Hivatkozás másolása a vágólapra" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "InlineMessage|" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Bezárás" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "A(z) %1 ablakkezelő rendszer" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (fordítva ezzel: %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Bezárás" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Beállítások" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Beállítások — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Jelenlegi oldal. Folyamat: %1 százalék." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Ugrás ide: %1. Folyamat. %2 százalék." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Jelenlegi oldal." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Ugrás ide: %1. Beavatkozást igényel." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Ugrás ide: %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "További műveletek" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Ugrás a(z) %1 KDE Store oldalára" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Hivatkozás címének másolása" + +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Keresés…" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "További műveletek" diff --git a/poqm/ia/libkirigami6_qt.po b/poqm/ia/libkirigami6_qt.po new file mode 100644 index 0000000..189024d --- /dev/null +++ b/poqm/ia/libkirigami6_qt.po @@ -0,0 +1,343 @@ +# SPDX-FileCopyrightText: 2017, 2019, 2020, 2021, 2022, 2023, 2024, 2025 giovanni +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2025-03-12 09:08+0100\n" +"Last-Translator: giovanni \n" +"Language-Team: Interlingua \n" +"Language: ia\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 23.08.5\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Invia un message de e-posta a %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Sea involvite" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Dona" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Reporta un bug" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licentia:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licentia: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Bibliothecas in uso" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autores" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Monstra photos de autor" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Gratias" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Traductores" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "A proposito de %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Abandona" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Ulterior Actiones" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Remove etiquetta" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Actiones" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Monstra adjuta de contexto" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Retro" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Claude barra lateral" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Aperi barra lateral" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Cargante..." + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Contrasigno" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Cela Contrasigno" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Monstra Contrasigno" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Claude menu" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Aperi menu" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Cerca…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Cerca" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Netta cerca" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Copia" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Selige toto" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Successo" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Aviso" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Error" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Nota" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Claude" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Claude designator" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Aperi designator" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Claude" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navigation de retro" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Naviga Avante" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Aperi ligamine %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Aperi ligamine" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copia ligame a area de transferentia" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Claude" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Le %1 systema de fenestra" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (construite sur %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Claude" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Preferentias" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Preferentias — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Pagina currente. Progresso: %1 percent" + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Naviga a %1. Progresso: %2 percent." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Pagina currente." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Navigante a %1. Demandante attention." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Naviga a %1" + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Ulterior Actiones" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Visita pagina %1 de KDE Store" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Copia adresse de ligamine" + +#~ msgctxt "LoadingPlaceholder|" +#~ msgid "Still loading, please wait." +#~ msgstr "Ancora cargante, pro favor, tu attende." + +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Cerca..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#, fuzzy +#~| msgctxt "ContextDrawer|" +#~| msgid "Actions" +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Actiones" diff --git a/poqm/id/libkirigami6_qt.po b/poqm/id/libkirigami6_qt.po new file mode 100644 index 0000000..c6684ff --- /dev/null +++ b/poqm/id/libkirigami6_qt.po @@ -0,0 +1,365 @@ +# Wantoyo , 2018, 2019, 2020, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-09-27 23:06+0700\n" +"Last-Translator: Wantoyèk \n" +"Language-Team: Indonesian \n" +"Language: id\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 21.12.3\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Kirim sebuah email ke %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Ikut Terlibat" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "" + +#: controls/AboutItem.qml:245 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Report Bug…" +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Laporkan Bug..." + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Hak Cipta" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Lisensi:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Lisensi: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Pustaka lib yang digunakan" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Penulis" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Tampilkan foto penulis" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Kredit" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Penerjemah" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Tentang %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Berhenti" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Aksi Selebihnya" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Hapus Tag" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Aksi" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Mundur" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Tutup Bilah Sisi" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Buka Bilah Sisi" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Memuat..." + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Password" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Password" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Password" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close drawer" +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Tutup penggambar" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Cari..." + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Cari" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "" + +#: controls/SelectableLabel.qml:179 +#, fuzzy +#| msgctxt "AboutItem|" +#| msgid "Copyright" +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Hak Cipta" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close drawer" +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Tutup penggambar" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Tutup penggambar" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Buka penggambar" + +#: controls/templates/OverlaySheet.qml:290 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close drawer" +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Tutup penggambar" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navigasi Mundur" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navigasi Maju" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Salin Tautan ke Papan Klip" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close drawer" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Tutup penggambar" + +#: platform/settings.cpp:219 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "KDE Frameworks %1" +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "The %1 windowing system" +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Sistem perjendelaan %1" + +#: platform/settings.cpp:222 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "Qt %2 (built against %3)" +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (dibangun terhadap %3)" + +#, fuzzy +#~| msgctxt "OverlayDrawer|" +#~| msgid "Close drawer" +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Tutup penggambar" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Pengaturan" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Pengaturan — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Halaman saat ini. Progres: %1 persen." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Navigasi ke %1. Progres: %2 persen." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Halaman saat ini." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Navigasi ke %1. Memerlukan perhatian." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Navigasi ke %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Aksi Selebihnya" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Kunjungilah halaman KDE Store %1" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Salin alamat tautan" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Cari..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Aksi Selebihnya" diff --git a/poqm/is/libkirigami6_qt.po b/poqm/is/libkirigami6_qt.po new file mode 100644 index 0000000..e60cc7f --- /dev/null +++ b/poqm/is/libkirigami6_qt.po @@ -0,0 +1,279 @@ +# SPDX-FileCopyrightText: 2024 Guðmundur Erlingsson +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2024-12-08 17:44+0000\n" +"Last-Translator: Gummi \n" +"Language-Team: Icelandic \n" +"Language: is\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 23.08.5\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Senda tölvupóst til %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Taka þátt" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Styrkja" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Senda villutilkynningu" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Höfundarréttur" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Notkunarleyfi:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Notkunarleyfi: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Hugbúnaðarsöfn í notkun" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Höfundar" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Sýna myndir af höfundi" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Þátttakendur" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Þýðendur" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Um %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Hætta" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Fleiri aðgerðir" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Fjarlægja merki" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Aðgerðir" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Sýna samhengishjálp" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Til baka" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Loka hliðarstiku" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Opna hliðarstiku" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Hleð inn…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Lykilorð" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Fela lykilorð" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Sýna lykilorð" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Loka valmynd" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Opna valmynd" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Leita…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Leita" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Hreinsa leit" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Afrita" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Velja allt" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Tókst" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Viðvörun" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Villa" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Athugasemd" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Loka" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Loka skúffu" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Opna skúffu" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Loka" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Fletta til baka" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Fletta áfram" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Opna tengil %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Opna tengil" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Afrita tengil á klippispjald" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "InlineMessage|" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Loka" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "%1 gluggakerfið" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (byggt út frá %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Loka" diff --git a/poqm/it/libkirigami6_qt.po b/poqm/it/libkirigami6_qt.po new file mode 100644 index 0000000..1dcf03c --- /dev/null +++ b/poqm/it/libkirigami6_qt.po @@ -0,0 +1,336 @@ +# SPDX-FileCopyrightText: 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025 Vincenzo Reale +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2025-03-12 16:23+0100\n" +"Last-Translator: Vincenzo Reale \n" +"Language-Team: Italian \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 24.12.3\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Invia un messaggio di posta elettronica a %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Partecipa" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Fai una donazione" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Segnala un bug" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licenza:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licenza: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Librerie in uso" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autori" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Mostra le foto degli autori" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Riconoscimenti" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Traduttori" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Informazioni su %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Esci" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Altre azioni" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Rimuovi etichetta" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Azioni" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Mostra la guida contestuale" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Indietro" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Chiudi la barra laterale" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Apri la barra laterale" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Caricamento…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Password" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Nascondi la password" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Mostra la password" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Chiudi il menu" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Apri il menu" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Cerca…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Cerca" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Cancella la ricerca" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Copia" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Seleziona tutto" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Successo" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Avviso" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Errore" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Nota" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Chiudi" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Chiudi il cassetto" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Apri il cassetto" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Chiudi" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Naviga indietro" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Naviga in avanti" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Apri il collegamento %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Apri il collegamento" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copia il collegamento negli appunti" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Chiudi" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Il sistema di gestione delle finestre %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (compilato con %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Chiudi" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Impostazioni" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Impostazioni — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Pagina attuale. Avanzamento: %1 percento." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Vai a %1. Avanzamento: %2 percento." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Pagina attuale." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Vai a %1. Richiede attenzione." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Vai a %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Altre azioni" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Visita la pagina del KDE Store di %1" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Copia indirizzo del collegamento" + +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Cerca..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Altre azioni" diff --git a/poqm/ja/libkirigami6_qt.po b/poqm/ja/libkirigami6_qt.po new file mode 100644 index 0000000..5ed9ea5 --- /dev/null +++ b/poqm/ja/libkirigami6_qt.po @@ -0,0 +1,308 @@ +# Ryuichi Yamada , 2023. +msgid "" +msgstr "" +"Project-Id-Version: libkirigamiplugin_qt\n" +"PO-Revision-Date: 2023-02-22 22:44+0900\n" +"Last-Translator: Ryuichi Yamada \n" +"Language-Team: Japanese \n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: qtrich\n" +"X-Qt-Contexts: true\n" +"X-Generator: Lokalize 22.12.2\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "%1 にメールを送る" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "参加する" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "寄付する" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "バグを報告" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "著作権について" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "ライセンス:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "ライセンス: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "使用されているライブラリ" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "作者" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "作者の写真を表示" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "クレジット" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "翻訳者" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1 について" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "終了" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "その他のアクション" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "タグを削除" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "アクション" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "戻る" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "サイドバーを閉じる" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "サイドバーを開く" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "読み込み中..." + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "パスワード" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "パスワード" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "パスワード" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "メニューを閉じる" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "メニューを開く" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "検索..." + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "検索" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "検索をクリア" + +#: controls/SelectableLabel.qml:179 +#, fuzzy +#| msgctxt "AboutItem|" +#| msgid "Copyright" +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "著作権について" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +#, fuzzy +#| msgctxt "PageRowGlobalToolBarUI|" +#| msgid "Close menu" +msgctxt "InlineMessage|" +msgid "Close" +msgstr "メニューを閉じる" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "ドロアを閉じる" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "ドロアを開く" + +#: controls/templates/OverlaySheet.qml:290 +#, fuzzy +#| msgctxt "PageRowGlobalToolBarUI|" +#| msgid "Close menu" +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "メニューを閉じる" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "戻る" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "進む" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +#, fuzzy +#| msgctxt "PageRowGlobalToolBarUI|" +#| msgid "Open menu" +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "メニューを開く" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "リンクをクリップボードにコピー" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "PageRowGlobalToolBarUI|" +#| msgid "Close menu" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "メニューを閉じる" + +#: platform/settings.cpp:219 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "KDE Frameworks %1" +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "The %1 windowing system" +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "%1 ウィンドウシステム" + +#: platform/settings.cpp:222 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "Qt %2 (built against %3)" +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (%3 を用いてビルドされました)" + +#, fuzzy +#~| msgctxt "PageRowGlobalToolBarUI|" +#~| msgid "Close menu" +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "メニューを閉じる" diff --git a/poqm/ka/libkirigami6_qt.po b/poqm/ka/libkirigami6_qt.po new file mode 100644 index 0000000..a5e21cb --- /dev/null +++ b/poqm/ka/libkirigami6_qt.po @@ -0,0 +1,316 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: Temuri Doghonadze \n" +"Language-Team: \n" +"Language: ka\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Qt-Contexts: true\n" +"X-Generator: Poedit 3.5\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "%1-სთვის ელფოსტის გაგზავნა" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "შემოგვიერთდით" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "შემოწირულობა" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "შეცდომის პატაკი" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "საავტორო უფლებები" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "ლიცენზია:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "ლიცენზია: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "გამოყენებული ბიბლიოთეკები" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "ავტორები" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "ავტორის ფოტოების ჩვენება" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "კრედიტები" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "მთარგმნელები" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1-ის შესახებ" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "დასრულება" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "მეტი ქმედება" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "ჭდის წაშლა" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "ქმედებები" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "კონტექსტური დახმარების ჩვენება" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "უკან" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "გვერდითი ზოლის დახურვა" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "გვერდითი ზოლის გახსნა" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "ჩატვირთვა…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "პაროლი" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "პაროლის დამალვა" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "პაროლის ჩვენება" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "მენიუს დახურვა" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "მენიუს გახსნა" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "ძებნა…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "ძებნა" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "ძიების გასუფთავება" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "კოპირება" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "ყველას მონიშვნა" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "წარმატება" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "გაფრთხილება" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "შეცდომა" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "შენიშვნა" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "დახურვა" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "უჯრის დახურვა" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "უჯრის გახსნა" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "დახურვა" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "უკან გადასვლა" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "წინ გადასვლა" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "ბმულის გახსნა %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "ბმულის გახსნა" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "ბმულის ბმულის ბაფერში კოპირება" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "დახურვა" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "ფანჯრული სისტემა %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (აგებულია %3-ით)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "დახურვა" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "&მორგება" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "პარამეტრები — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "მიმდინარე გვერდი. პროგრესი: %1 პროცენტი." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "%1-ზეგადასვლა. პროგრესი: %2 პროცენტი." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "მიმდინარე გვერდი." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "%1-ზე გადასვლა. საჭიროა ყურადღება." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "%1-ზე გადასვლა." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "მეტი ქმედება" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "ეწვიეთ %1-ის KDE მაღაზიის გვერდს" diff --git a/poqm/ko/libkirigami6_qt.po b/poqm/ko/libkirigami6_qt.po new file mode 100644 index 0000000..0794d0b --- /dev/null +++ b/poqm/ko/libkirigami6_qt.po @@ -0,0 +1,343 @@ +# SPDX-FileCopyrightText: 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024 Shinjo Park +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2024-12-24 16:59+0100\n" +"Last-Translator: Shinjo Park \n" +"Language-Team: Korean \n" +"Language: ko\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Lokalize 23.08.5\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1(%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "%1(으)로 이메일 보내기" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "참여하기" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "기부하기" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "버그 보고" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "저작권" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "라이선스:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "라이선스: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "사용하는 라이브러리" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "작성자" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "작성자 사진 표시" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "제작진" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "번역자" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1 정보" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "끝내기" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "더 많은 동작" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "태그 삭제" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "동작" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "문맥 도움말 표시" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "뒤로" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "사이드바 닫기" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "사이드바 열기" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "불러오는 중…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "암호" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "암호 숨기기" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "암호 표시" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "메뉴 닫기" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "메뉴 열기" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "검색…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "검색" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "검색 지우기" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "복사" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "모두 선택" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "성공" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "경고" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "오류" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "메모" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "닫기" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "서랍 닫기" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "서랍 열기" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "닫기" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "이전 탐색" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "다음 탐색" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "%1 링크 열기" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "링크 열기" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "클립보드에 링크 복사" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "InlineMessage|" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "닫기" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE 프레임워크 %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "%1 창 시스템" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2(%3(으)로 빌드됨)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "닫기" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "설정" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "설정 — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "현재 쪽입니다. 진행 상황: %1퍼센트." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "%1(으)로 탐색합니다. 진행 상황: %2퍼센트." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "현재 쪽입니다." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "%1(으)로 탐색합니다. 주목을 기다립니다." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "%1(으)로 탐색합니다." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "더 많은 동작" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "%1의 KDE 스토어 페이지 방문" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "링크 주소 복사" + +#~ msgctxt "LoadingPlaceholder|" +#~ msgid "Still loading, please wait." +#~ msgstr "불러오고 있습니다. 잠시 기다려 주십시오." + +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "찾기..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "더 많은 동작" diff --git a/poqm/lt/libkirigami6_qt.po b/poqm/lt/libkirigami6_qt.po new file mode 100644 index 0000000..f35d8d3 --- /dev/null +++ b/poqm/lt/libkirigami6_qt.po @@ -0,0 +1,316 @@ +msgid "" +msgstr "" +"Project-Id-Version: trunk-kf 5\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: Moo\n" +"Language-Team: lt\n" +"Language: lt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n%10>=2 && (n%100<10 || n" +"%100>=20) ? 1 : n%10==0 || (n%100>10 && n%100<20) ? 2 : 3);\n" +"X-Qt-Contexts: true\n" +"X-Generator: Poedit 3.4.2\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Siųsti el. laišką, adresu %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Įsitraukti" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Paremti" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Pranešti apie klaidą" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Autorių teisės" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licencija:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licencija: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Naudojamos bibliotekos" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autoriai" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Rodyti autorių nuotraukas" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Padėkos" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Vertėjai" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Apie %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Išeiti" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Daugiau veiksmų" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Šalinti žymę" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Veiksmai" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Rodyti kontekstinę pagalbą" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Atgal" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Užverti šoninę juostą" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Atverti šoninę juostą" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Įkeliama…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Slaptažodis" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Slėpti slaptažodį" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Rodyti slaptažodį" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Užverti meniu" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Atverti meniu" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Ieškoti…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Paieška" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Išvalyti paiešką" + +#: controls/SelectableLabel.qml:179 +#, fuzzy +#| msgctxt "AboutItem|" +#| msgid "Copyright" +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Autorių teisės" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +#, fuzzy +#| msgctxt "Dialog|@action:button close dialog" +#| msgid "Close" +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Užverti" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Uždaryti stalčių" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Atidaryti stalčių" + +#: controls/templates/OverlaySheet.qml:290 +#, fuzzy +#| msgctxt "Dialog|@action:button close dialog" +#| msgid "Close" +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Užverti" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Naršyti atgal" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Naršyti pirmyn" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Atverti nuorodą %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Atverti nuorodą" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Kopijuoti nuorodą į iškarpinę" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "Dialog|@action:button close dialog" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Užverti" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "%1 langų sistema" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (sudaryta remiantis %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Užverti" + +#, fuzzy +#~| msgctxt "ForwardButton|" +#~| msgid "Navigate Forward" +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Naršyti pirmyn" + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Daugiau veiksmų" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Kopijuoti nuorodos adresą" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Ieškoti..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Daugiau veiksmų" diff --git a/poqm/lv/libkirigami6_qt.po b/poqm/lv/libkirigami6_qt.po new file mode 100644 index 0000000..f50e237 --- /dev/null +++ b/poqm/lv/libkirigami6_qt.po @@ -0,0 +1,280 @@ +# SPDX-FileCopyrightText: 2024, 2025 Toms Trasuns +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2025-01-04 15:15+0200\n" +"Last-Translator: Toms Trasuns \n" +"Language-Team: Latvian \n" +"Language: lv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : " +"2);\n" +"X-Generator: Lokalize 24.12.0\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Sūtīt e-pasta vēstuli „%1“" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Iesaistīties" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Ziedot" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Ziņot par kļūdu" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Autortiesības" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licence:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licence: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Izmantotās bibliotēkas" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autori" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Rādīt autoru fotogrāfijas" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Veidotāji" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Tulkotāji" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Par „%1“" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Iziet" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Vairāk darbību" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Noņemt birku" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Darbības" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Rādīt kontekstuālu palīdzību" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Atpakaļ" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Aizvērt sānu joslu" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Atvērt sānu joslu" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Ielādē…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Parole" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Slēpt paroli" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Rādīt paroli" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Aizvērt izvēlni" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Atvērt izvēlni" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Meklēt…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Meklēt" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Notīrīt meklēšanu" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Kopēt" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Atlasīt visu" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Izdevās" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Uzmanību" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Kļūda" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Piezīme" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Aizvērt" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Aizvērt atvilktni" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Atvērt atvilktni" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Aizvērt" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Iet atpakaļ" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Iet uz priekšu" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Atvērt saiti %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Atvērt saiti" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Kopēt saiti starpliktuvē" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "InlineMessage|" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Aizvērt" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE satvari %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "„%1“ logu sistēma" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (būvēts pret %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Aizvērt" diff --git a/poqm/ml/libkirigami6_qt.po b/poqm/ml/libkirigami6_qt.po new file mode 100644 index 0000000..9aa9d05 --- /dev/null +++ b/poqm/ml/libkirigami6_qt.po @@ -0,0 +1,270 @@ +msgid "" +msgstr "" +"Project-Id-Version: libkirigami2plugin_qt\n" +"Last-Translator: Automatically generated\n" +"Language-Team: Swathanthra|സ്വതന്ത്ര Malayalam|മലയാളം Computing|കമ്പ്യൂട്ടിങ്ങ് \n" +"Language: ml\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "" diff --git a/poqm/nb/libkirigami6_qt.po b/poqm/nb/libkirigami6_qt.po new file mode 100644 index 0000000..ef1d302 --- /dev/null +++ b/poqm/nb/libkirigami6_qt.po @@ -0,0 +1,276 @@ +# Translation of libkirigami6_qt to Norwegian Bokmål +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2025-03-13 15:04+0100\n" +"Last-Translator: Martin Hansen \n" +"Language-Team: Norwegian Bokmål \n" +"Language: nb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 24.12.3\n" +"X-Environment: kde\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: qtrich\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Send e-post til %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Bli med" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Gi pengegave" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Meld fra om feil" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Opphavsrett" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Lisens:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Lisens: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Brukte bibliotek" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Opphavspersoner" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Vis bilde av opphavspersonene" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Bidragsytere" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Oversettere" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Om %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Avslutt" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Flere handlinger" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Fjern merkelapp" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Handlinger" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Vis emnehjelp" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Tilbake" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Lukk sidestolpen" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Åpne sidestolpe" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Laster …" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Passord" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Skjul passord" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Vis passord" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Lukk meny" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Åpne meny" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Søk …" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Søk" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Tøm søkefelt" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Kopier" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Merk alle" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Vellykket" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Advarsel" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Feil" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Merk" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Lukk" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Lukk skuff" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Åpne skuff" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Lukk" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Naviger tilbake" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Naviger frem" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Åpne lenka %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Åpne lenka" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Kopier lenka til utklippstavla" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Lukk" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Vindussystemet %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (bygd mot %3)" diff --git a/poqm/nl/libkirigami6_qt.po b/poqm/nl/libkirigami6_qt.po new file mode 100644 index 0000000..260c8d3 --- /dev/null +++ b/poqm/nl/libkirigami6_qt.po @@ -0,0 +1,340 @@ +# SPDX-FileCopyrightText: 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025 Freek de Kruijf +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2025-03-12 10:14+0100\n" +"Last-Translator: Freek de Kruijf \n" +"Language-Team: \n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 24.12.3\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Een e-mail sturen naar %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Doe mee" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Doneren" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Een bug rapporteren" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licentie:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licentie: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Bibliotheken in gebruik" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Auteurs" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Foto's van auteurs tonen" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Dankbetuigingen" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Vertalers" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Info over %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Afsluiten" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Meer acties" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Tag verwijderen" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Acties" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Contextuele hulp tonen" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Terug" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Zijbalk sluiten" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Zijbalk openen" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Bezig met laden…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Wachtwoord" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Wachtwoord verbergen" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Wachtwoord tonen" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Menu sluiten" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Menu openen" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Zoeken…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Zoeken" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Zoeken wissen" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Kopie" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Alles selecteren" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Succes" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Waarschuwing" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Fout" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Notitie" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Sluiten" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Schuiflade sluiten" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Schuiflade openen" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Sluiten" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Naar achteren navigeren" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Naar voren navigeren" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Koppeling %1 openen" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Koppeling openen" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Koppeling naar klembord kopiëren" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Sluiten" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Het venstersysteem %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (gebouwd tegen %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Sluiten" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Instellingen" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Instellingen — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Huidige pagina. Voortgang: %1 procent." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Naar %1 navigeren. Voortgang: %2 procent." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Huidige pagina." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Naar %1 navigeren. Vraagt om aandacht." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Naar %1 navigeren." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Meer acties" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Bezoek de KDE Store-pagina van %1" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Koppelingsadres kopiëren" + +#~ msgctxt "LoadingPlaceholder|" +#~ msgid "Still loading, please wait." +#~ msgstr "Nog steeds bezig met laden, even geduld." + +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Zoeken..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Meer acties" diff --git a/poqm/nn/libkirigami6_qt.po b/poqm/nn/libkirigami6_qt.po new file mode 100644 index 0000000..7b36517 --- /dev/null +++ b/poqm/nn/libkirigami6_qt.po @@ -0,0 +1,278 @@ +# Translation of libkirigami6_qt to Norwegian Nynorsk +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2024-10-27 14:43+0100\n" +"Last-Translator: Karl Ove Hufthammer \n" +"Language-Team: Norwegian Nynorsk \n" +"Language: nn\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 24.11.70\n" +"X-Environment: kde\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: qtrich\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Send e-post til %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Vert med" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Gje pengegåve" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Meld frå om feil" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Opphavsrett" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Lisens:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Lisens: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Brukte bibliotek" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Opphavspersonar" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Vis bilete av opphavspersonane" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Kreditering" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Omsetjarar" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Om %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Avslutt" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Fleire handlingar" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Fjern merkelapp" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Handlingar" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Vis emnehjelp" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Tilbake" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Lukk sidestolpen" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Opna sidestolpe" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Lastar …" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Passord" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Gøym passord" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Vis passord" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Lukk meny" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Opna meny" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Søk …" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Søk" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Tøm søkjefelt" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Kopier" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Merk alle" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +#, fuzzy +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Lukk" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Lukk skuff" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Opna skuff" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Lukk" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Naviger tilbake" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Naviger fram" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Opna lenkja %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Opna lenkja" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Kopier lenkja til utklippstavla" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Lukk" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Vindaugssystemet %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (bygd mot %3)" diff --git a/poqm/pa/libkirigami6_qt.po b/poqm/pa/libkirigami6_qt.po new file mode 100644 index 0000000..ae08239 --- /dev/null +++ b/poqm/pa/libkirigami6_qt.po @@ -0,0 +1,350 @@ +# A S Alam , 2021, 2023. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2023-04-01 20:53-0700\n" +"Last-Translator: A S Alam \n" +"Language-Team: Punjabi \n" +"Language: pa\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 22.12.3\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "%1 ਨੂੰ ਈਮੇਲ ਭੇਜੋ" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "ਹਿੱਸਾ ਲਵੋ" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "ਦਾਨ ਕਰੋ" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "ਬੱਗ ਦੀ ਜਾਣਕਾਰੀ ਦਿਓ" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "ਕਾਪੀਰਾਈਟ" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "ਲਸੰਸ" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "ਲਸੰਸ: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "ਵਰਤਣ ਲਈ ਲਾਇਬਰੇਰੀਆਂ" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "ਲੇਖਕ" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "ਲੇਖਕ ਦੀ ਫ਼ੋਟੋ ਵੇਖਾਓ" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "ਮਾਣ" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "ਉਲੱਥਾਕਾਰ" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1 ਬਾਰੇ" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "ਬਾਹਰ" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "ਹੋਰ ਕਾਰਵਾਈਆਂ" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "ਟੈਗ ਹਟਾਓ" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "ਕਾਰਵਾਈਆਂ" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "ਪਿੱਛੇ" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "ਬਾਹੀ ਬੰਦ ਕਰੋ" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "ਬਾਹੀ ਖੋਲ੍ਹੋ" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "…ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "ਪਾਸਵਰਡ" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "ਪਾਸਵਰਡ" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "ਪਾਸਵਰਡ" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "ਮੇਨੂ ਬੰਦ ਕਰੋ" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "ਮੇਨੂ ਖੋਲ੍ਹੋ" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "…ਖੋਜੋ" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "ਖੋਜੋ" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "ਖੋਜ ਮਿਟਾਓ" + +#: controls/SelectableLabel.qml:179 +#, fuzzy +#| msgctxt "AboutItem|" +#| msgid "Copyright" +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "ਕਾਪੀਰਾਈਟ" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +#, fuzzy +#| msgctxt "PageRowGlobalToolBarUI|" +#| msgid "Close menu" +msgctxt "InlineMessage|" +msgid "Close" +msgstr "ਮੇਨੂ ਬੰਦ ਕਰੋ" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "ਦਰਾਜ ਬੰਦ ਕਰੋ" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "ਦਰਾਜ ਖੋਲ੍ਹੋ" + +#: controls/templates/OverlaySheet.qml:290 +#, fuzzy +#| msgctxt "PageRowGlobalToolBarUI|" +#| msgid "Close menu" +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "ਮੇਨੂ ਬੰਦ ਕਰੋ" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "ਪਿੱਛੇ ਜਾਓ" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "ਅੱਗੇ ਜਾਓ" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +#, fuzzy +#| msgctxt "PageRowGlobalToolBarUI|" +#| msgid "Open menu" +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "ਮੇਨੂ ਖੋਲ੍ਹੋ" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "ਲਿੰਕ ਕਲਿੱਪਬੋਰਡ ਵਿੱਚ ਕਾਪੀ ਕਰੋ" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "PageRowGlobalToolBarUI|" +#| msgid "Close menu" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "ਮੇਨੂ ਬੰਦ ਕਰੋ" + +#: platform/settings.cpp:219 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "KDE Frameworks %1" +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE ਫਰੇਮਵਰਕਸ %1" + +#: platform/settings.cpp:221 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "The %1 windowing system" +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "%1 ਵਿੰਡੋਇੰਗ ਸਿਸਟਮ" + +#: platform/settings.cpp:222 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "Qt %2 (built against %3)" +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (%3 ਨਾਲ ਬਣਾਇਆ)" + +#, fuzzy +#~| msgctxt "PageRowGlobalToolBarUI|" +#~| msgid "Close menu" +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "ਮੇਨੂ ਬੰਦ ਕਰੋ" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "ਸੈਟਿੰਗਾਂ" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "ਸੈਟਿੰਗਾਂ — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "ਮੌਜੂਦਾ ਸਫ਼ਾ। ਤਰੱਕੀ: %1 ਫ਼ੀਸਦੀ।" + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "%1 ਉੱਤੇ ਜਾਓ। ਤਰੱਕੀ: %2 ਫ਼ੀਸਦੀ।" + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "ਮੌਜੂਦਾ ਸਫ਼ਾ।" + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "%1 ਉੱਤੇ ਜਾਓ। ਧਿਆਨ ਦੇਣ ਦੀ ਲੋੜ ਹੈ।" + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "%1 ਉੱਤੇ ਜਾਓ।" + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "ਹੋਰ ਕਾਰਵਾਈਆਂ" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "ਲਿੰਕ ਐਡਰੈਸ ਕਾਪੀ ਕਰੋ" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "ਖੋਜੋ..." diff --git a/poqm/pl/libkirigami6_qt.po b/poqm/pl/libkirigami6_qt.po new file mode 100644 index 0000000..c33e26b --- /dev/null +++ b/poqm/pl/libkirigami6_qt.po @@ -0,0 +1,337 @@ +# SPDX-FileCopyrightText: 2016, 2017, 2018, 2019, 2021, 2022, 2023, 2024, 2025 Łukasz Wojniłowicz +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2025-03-15 10:46+0100\n" +"Last-Translator: Łukasz Wojniłowicz \n" +"Language-Team: Polish \n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" +"X-Generator: Lokalize 24.12.3\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Wyślij wiadomość do %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Współtwórz" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Darowizna" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Zgłoś błąd" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Prawa autorskie" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licencja:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licencja: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Używane biblioteki" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autorzy" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Pokaż zdjęcia autorów" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Zasługi" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Tłumacze" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "O %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Zakończ" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Więcej działań" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Usuń znacznik" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Działania" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Pokaż pomoc podręczną" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Wstecz" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Zamknij pasek boczny" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Otwórz pasek boczny" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Wczytywanie..." + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Hasło" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Ukryj hasło" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Pokaż hasło" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Zamknij menu" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Otwórz menu" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Poszukaj..." + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Szukaj" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Wyczyść wyszukiwanie" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Skopiuj" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Zaznacz wszystko" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Powodzenie" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Ostrzeżenie" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Błąd" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Uwaga" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Zamknij" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Zamknij szufladę" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Otwórz szufladę" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Zamknij" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Przejdź wstecz" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Przejdź naprzód" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Otwórz odnośnik %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Otwórz odnośnik" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Skopiuj odnośnik do schowka" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Zamknij" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "System okien %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (zbudowany na %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Zamknij" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Ustawienia" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Ustawienia — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Bieżąca strona. Postęp: %1 procent." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Przejdź do %1. Postęp: %2 procent." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Bieżąca strona." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Przejdź do %1. Wymaga uwagi." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Przejdź do %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Więcej działań" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Odwiedź stronę sklepu KDE %1" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Skopiuj adres odnośnika" + +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Szukaj..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Więcej działań" diff --git a/poqm/pt/libkirigami6_qt.po b/poqm/pt/libkirigami6_qt.po new file mode 100644 index 0000000..30945b5 --- /dev/null +++ b/poqm/pt/libkirigami6_qt.po @@ -0,0 +1,348 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR Free Software Foundation, Inc. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: libkirigami2plugin_qt\n" +"PO-Revision-Date: 2022-12-23 20:43+0000\n" +"Last-Translator: José Nuno Coelho Pires \n" +"Language-Team: Portuguese \n" +"Language: pt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Enviar um e-mail para %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Envolva-se" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Doar" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Comunicar um Erro" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "'Copyright'" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licença:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licença: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Bibliotecas usadas" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autores" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Mostrar as fotografias dos autores" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Créditos" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Tradutores" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Acerca do %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Sair" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Mais Acções" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Remover a Marca" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Acções" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Recuar" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Fechar a Barra Lateral" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Abrir a Barra Lateral" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "A carregar…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Senha" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Senha" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Senha" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Fechar o menu" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Abrir o menu" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Procurar…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Procurar" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Limpar a pesquisa" + +#: controls/SelectableLabel.qml:179 +#, fuzzy +#| msgctxt "AboutItem|" +#| msgid "Copyright" +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "'Copyright'" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +#, fuzzy +#| msgctxt "PageRowGlobalToolBarUI|" +#| msgid "Close menu" +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Fechar o menu" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Fechar a área" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Abrir a área" + +#: controls/templates/OverlaySheet.qml:290 +#, fuzzy +#| msgctxt "PageRowGlobalToolBarUI|" +#| msgid "Close menu" +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Fechar o menu" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navegar para Trás" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navegar para a Frente" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +#, fuzzy +#| msgctxt "PageRowGlobalToolBarUI|" +#| msgid "Open menu" +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Abrir o menu" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copiar a Ligação para a Área de Transferência" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "PageRowGlobalToolBarUI|" +#| msgid "Close menu" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Fechar o menu" + +#: platform/settings.cpp:219 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "KDE Frameworks %1" +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "Plataformas do KDE %1" + +#: platform/settings.cpp:221 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "The %1 windowing system" +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "O sistema de janelas %1" + +#: platform/settings.cpp:222 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "Qt %2 (built against %3)" +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (compilado com o %3)" + +#, fuzzy +#~| msgctxt "PageRowGlobalToolBarUI|" +#~| msgid "Close menu" +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Fechar o menu" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Configuração" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Configuração ̣̣— %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Página actual. Progresso: %1 por cento." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Navegar para %1. Progresso: %2 por cento." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Página actual." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Navegar para %1. A chamar a atenção." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Navegar para %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Mais Acções" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Visitar a página do %1 na Loja do KDE" diff --git a/poqm/pt_BR/libkirigami6_qt.po b/poqm/pt_BR/libkirigami6_qt.po new file mode 100644 index 0000000..ceb2fe2 --- /dev/null +++ b/poqm/pt_BR/libkirigami6_qt.po @@ -0,0 +1,344 @@ +# Translation of libkirigami2plugin_qt.po to Brazilian Portuguese +# Copyright (C) 2016-2019 This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Luiz Fernando Ranghetti , 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023. +# André Marcelo Alvarenga , 2018, 2019. +# Thiago Masato Costa Sueto , 2021. +# SPDX-FileCopyrightText: 2023 Geraldo Simiao +# SPDX-FileCopyrightText: 2025 Guilherme Marçal Silva +msgid "" +msgstr "" +"Project-Id-Version: libkirigami2plugin_qt\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"PO-Revision-Date: 2025-02-06 00:28-0300\n" +"Last-Translator: Guilherme Marçal Silva \n" +"Language-Team: Brazilian Portuguese \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Lokalize 24.12.1\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Enviar e-mail para %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Participe" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Doar" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Relatar erro" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licença:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licença: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Bibliotecas em uso" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autores" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Mostrar as fotos dos autores" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Créditos" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Tradutores" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Sobre %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Sair" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Mais ações" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Remover etiqueta" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Ações" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Exibir ajuda contextual" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Voltar" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Fechar barra lateral" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Abrir barra lateral" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Carregando..." + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Senha" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Ocultar senha" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Mostrar senha" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Fechar menu" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Abrir menu" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Pesquisar..." + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Pesquisar" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Limpar pesquisa" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Copiar" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Selecionar tudo" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Sucesso" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Aviso" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Erro" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Nota" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Fechar" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Fechar gaveta" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Abrir gaveta" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Fechar" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Voltar" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Avançar" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Abrir link %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr " Abrir link" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copiar link para a área de transferência" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "InlineMessage|" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Fechar" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "O %1 sistema de janelas" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (compilado com %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Fechar" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Configurações" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Configurações — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Página atual. Progresso: %1 porcento." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Navegar para %1. Progresso: %2 porcento." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Página atual." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Navegar para %1. Demandando atenção." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Navegar para %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Mais ações" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Visite a página do %1 na KDE Store" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Copiar endereço do link" + +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Pesquisar..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" diff --git a/poqm/ro/libkirigami6_qt.po b/poqm/ro/libkirigami6_qt.po new file mode 100644 index 0000000..d9c6868 --- /dev/null +++ b/poqm/ro/libkirigami6_qt.po @@ -0,0 +1,371 @@ +# Sergiu Bivol , 2020, 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-12-28 20:59+0000\n" +"Last-Translator: Sergiu Bivol \n" +"Language-Team: Romanian \n" +"Language: ro\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " +"20)) ? 1 : 2;\n" +"X-Generator: Lokalize 21.12.3\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Trimite scrisoare către %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Implică-te" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "" + +#: controls/AboutItem.qml:245 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Report Bug…" +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Raportează defect…" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Drept de autor" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licență:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licență: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Biblioteci folosite" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autori" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Arată pozele autorilor" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Mulțumiri" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Traducători" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Despre %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Termină" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Acțiuni suplimentare" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Elimină marcajul" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Acțiuni" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Înapoi" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Închide bara laterală" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Deschide bara laterală" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Se încarcă…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Parolă" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Parolă" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Parolă" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Închide" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Open" +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Deschide" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Caută…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Caută" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "" + +#: controls/SelectableLabel.qml:179 +#, fuzzy +#| msgctxt "AboutItem|" +#| msgid "Copyright" +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Drept de autor" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Închide" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Închide sertarul" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Deschide sertarul" + +#: controls/templates/OverlaySheet.qml:290 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Închide" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navighează înapoi" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navighează înainte" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Open" +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Deschide" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copiază legătura în clipboard" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Închide" + +#: platform/settings.cpp:219 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "KDE Frameworks %1" +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "The %1 windowing system" +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Sistemul de ferestre %1" + +#: platform/settings.cpp:222 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "Qt %2 (built against %3)" +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (construit cu %3)" + +#, fuzzy +#~| msgctxt "OverlayDrawer|" +#~| msgid "Close" +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Închide" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Configurări" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Configurări — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Pagina actuală. Progres: %1 procente." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Navighează la %1. Progres: %2 procente." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Pagina actuală." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Navighează la %1. Cere atenție." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Navighează la %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Acțiuni suplimentare" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Vizitează pagina %1 din Magazinul KDE" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Copiază adresa legăturii" + +#, fuzzy +#~| msgctxt "AboutPage|" +#~| msgid "(%1)" +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Caută..." diff --git a/poqm/ru/libkirigami6_qt.po b/poqm/ru/libkirigami6_qt.po new file mode 100644 index 0000000..340c023 --- /dev/null +++ b/poqm/ru/libkirigami6_qt.po @@ -0,0 +1,339 @@ +# Alexander Potashev , 2016, 2017, 2019. +# Alexander Yavorsky , 2019, 2020, 2021, 2024. +# Мария Шикунова , 2022, 2023. +# SPDX-FileCopyrightText: 2024, 2025 Olesya Gerasimenko +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2025-02-28 16:43+0300\n" +"Last-Translator: Olesya Gerasimenko \n" +"Language-Team: Basealt Translation Team\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Lokalize 23.08.5\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Отправить письмо по адресу %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Присоединиться к команде" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Сделать пожертвование" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Сообщить об ошибке" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Авторские права" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Лицензия:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Лицензия: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Используемые библиотеки" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Авторы" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Показать фотографии авторов" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Благодарности" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Переводчики" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "О программе %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Выход" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Больше действий" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Удаление метки" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Действия" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Показать контекстную справку" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Назад" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Закрыть боковую панель" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Открыть боковую панель" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Загрузка…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Пароль" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Скрыть пароль" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Показать пароль" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Закрыть меню" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Открыть меню" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Поиск…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Поиск" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Очистить поиск" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Копировать" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Выбрать все" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Успешно" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Предупреждение" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Ошибка" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Примечание" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Закрыть" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Закрыть панель" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Открыть панель" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Закрыть" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Перейти назад" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Перейти вперёд" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Открыть ссылку %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Открыть ссылку" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Копировать ссылку в буфер обмена" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "InlineMessage|" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Закрыть" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Платформа графического сервера %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (собрана с версией %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Закрыть" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Параметры" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Параметры — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Текущая страница. Ход выполнения: %1%." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Переход к %1. Ход выполнения: %2%." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Текущая страница." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Перейти к %1. Требует внимания." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Перейти к %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Больше действий" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Перейти на страницу %1 в магазине приложений KDE" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Скопировать адрес ссылки" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Поиск..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Больше действий" diff --git a/poqm/sa/libkirigami6_qt.po b/poqm/sa/libkirigami6_qt.po new file mode 100644 index 0000000..0ad6371 --- /dev/null +++ b/poqm/sa/libkirigami6_qt.po @@ -0,0 +1,279 @@ +# SPDX-FileCopyrightText: 2024 kali +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2024-12-20 21:52+0530\n" +"Last-Translator: kali \n" +"Language-Team: Sanskrit \n" +"Language: sa\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=(n>1);\n" +"X-Generator: Lokalize 24.08.2\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "%1 इत्यस्मै ईमेल प्रेषयन्तु" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "सम्मिलित हो जाओ" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "वितरणं" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "एकं दोषं प्रतिवेदयतु" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "प्रतिलिपि अधिकार" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "अनुज्ञापत्रम् :" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "अनुज्ञापत्रम् : %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "प्रयुक्ताः पुस्तकालयाः" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "लेखकाः" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "लेखकस्य छायाचित्रं दर्शयतु" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "श्रेयः" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "अनुवादकाः" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1 इत्यस्य विषये" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "परिजहातु" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "अधिकानि क्रियाः" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "टैग हटाएँ" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "कर्माणि" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "सन्दर्भसहायतां दर्शयतु" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "पृष्ठभागः" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "पार्श्वपट्टिकां बन्दं कुर्वन्तु" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "पार्श्वपट्टिका उद्घाटयन्तु" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "लोडिंग…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "समाभाष्" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "गुप्तशब्दं गोपयन्तु" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "गुप्तशब्दं दर्शयतु" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "मेनू बन्दं कुर्वन्तु" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "मेनू उद्घाटयन्तु" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "अन्वेषण…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "अन्वेषण" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "अन्वेषणं स्पष्टं कुरुत" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "प्रतिलिपि" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "All इति चिनोतु" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "सफलता" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "चेतवानी" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "त्रुटि" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "टीका" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "पिधानं करोतु" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "दराजं बन्दं कुर्वन्तु" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "दराजं उद्घाटयन्तु" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "पिधानं करोतु" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Back इति नेविगेट् कुर्वन्तु" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "अग्रे गच्छन्तु" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "%1 इति लिङ्क् उद्घाटयन्तु" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "लिङ्कं उद्घाटयन्तु" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "क्लिप्बोर्ड् मध्ये लिङ्कं प्रतिलिख्यताम्" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "InlineMessage|" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "पिधानं करोतु" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE ढांचा %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "%1 विण्डोिंग् प्रणाली" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (%3 विरुद्धं निर्मितम्) ." + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "पिधानं करोतु" diff --git a/poqm/sk/libkirigami6_qt.po b/poqm/sk/libkirigami6_qt.po new file mode 100644 index 0000000..10341e8 --- /dev/null +++ b/poqm/sk/libkirigami6_qt.po @@ -0,0 +1,423 @@ +# translation of libkirigamiplugin_qt.po to Slovak +# Roman Paholik , 2016, 2019. +# Mthw , 2019. +# Matej Mrenica , 2019, 2020, 2021. +msgid "" +msgstr "" +"Project-Id-Version: libkirigamiplugin_qt\n" +"Report-Msgid-Bugs-To: http://bugs.kde.org\n" +"POT-Creation-Date: 2016-08-05 07:24+0000\n" +"PO-Revision-Date: 2021-09-05 19:34+0200\n" +"Last-Translator: Matej Mrenica \n" +"Language-Team: Slovak \n" +"Language: sk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 21.08.1\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:172 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "Send an email to %1" +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Odoslať email na adresu %1" + +#: controls/AboutItem.qml:222 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Get Involved" +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Zapojiť sa" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "" + +#: controls/AboutItem.qml:245 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Report Bug…" +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Nahlásiť chybu..." + +#: controls/AboutItem.qml:258 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:302 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "License:" +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licencia:" + +#: controls/AboutItem.qml:324 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "License: %1" +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licencia: %1" + +#: controls/AboutItem.qml:335 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Libraries in use" +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Použité knižnice" + +#: controls/AboutItem.qml:365 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Authors" +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autori" + +#: controls/AboutItem.qml:375 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Show author photos" +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Zobraziť fotografie autora" + +#: controls/AboutItem.qml:386 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Credits" +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Zásluhy" + +#: controls/AboutItem.qml:398 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Translators" +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Preklady" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "O %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Ukončiť" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Viac akcií" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Akcie" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Späť" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Zatvoriť bočný panel" + +#: controls/GlobalDrawer.qml:666 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Zatvoriť bočný panel" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Heslo" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Heslo" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Heslo" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Zatvoriť" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Open" +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Otvoriť" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Hľadať..." + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Hľadať" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "" + +#: controls/SelectableLabel.qml:179 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Copyright" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Zatvoriť" + +#: controls/templates/OverlayDrawer.qml:128 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Zatvoriť bočný panel" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/OverlaySheet.qml:290 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Zatvoriť" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navigovať späť" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navigovať dopredu" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Open" +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Otvoriť" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: dialogs/DialogHeaderTopContent.qml:89 +#, fuzzy +#| msgctxt "OverlayDrawer|" +#| msgid "Close" +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Zatvoriť" + +#: platform/settings.cpp:219 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "KDE Frameworks %1" +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworky %1" + +#: platform/settings.cpp:221 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "The %1 windowing system" +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "The %1 systém okien" + +#: platform/settings.cpp:222 +#, fuzzy, qt-format +#| msgctxt "Settings|" +#| msgid "Qt %2 (built against %3)" +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (zostavené s %3)" + +#, fuzzy +#~| msgctxt "OverlayDrawer|" +#~| msgid "Close" +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Zatvoriť" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Nastavenia" + +#, fuzzy +#~| msgctxt "CategorizedSettings|" +#~| msgid "Settings" +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Nastavenia" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Aktuálna stránka. Postup: %1 percent." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Navigovať do %1. Postup: %2 percent. " + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Aktuálna stránka." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Navigovať do %1. Vyžaduje si pozornosť." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Navigovať do %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Viac akcií" + +#, fuzzy +#~| msgctxt "AboutPage|" +#~| msgid "Visit %1's KDE Store page" +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Navštívte stránku obchodu KDE %1" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Skopírovať adresu odkazu" + +#, fuzzy +#~| msgctxt "AboutPage|" +#~| msgid "(%1)" +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Hľadať..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Viac akcií" diff --git a/poqm/sl/libkirigami6_qt.po b/poqm/sl/libkirigami6_qt.po new file mode 100644 index 0000000..58648c2 --- /dev/null +++ b/poqm/sl/libkirigami6_qt.po @@ -0,0 +1,330 @@ +# Slovenian translation of kirigami +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Andrej Mernik , 2016, 2018. +# Matjaž Jeran , 2019, 2020, 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: 2025-03-12 07:45+0100\n" +"Last-Translator: Matjaž Jeran \n" +"Language-Team: Slovenian \n" +"Language: sl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && n" +"%100<=4 ? 2 : 3);\n" +"X-Qt-Contexts: true\n" +"X-Generator: Poedit 3.5\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Pošljite e-pošto za %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Sodelujte" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Donirajte" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Poročajte o napaki" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Dovoljenje:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Dovoljenje: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Knjižnice v rabi" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Avtorji" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Prikaži fotografije avtorjev" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Zasluge" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Prevajalci" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "O programu %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Zapusti" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Več dejanj" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Odstrani značko" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Dejanja" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Prikaži kontekstno pomoč" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Nazaj" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Zapri stransko letvico" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Odpri stransko letvico" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Nalaganje…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Geslo" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Skrij geslo" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Prikaži geslo" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Zapri meni" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Odpri meni" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Poišči …" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Išči" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Počisti iskanje" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Kopiraj" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Izberi vse" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Uspeh" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Opozorilo" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Napaka" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Opomba" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Zapri" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Zapri predal" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Odpri predal" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Zapri" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Krmari nazaj" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Krmari naprej" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Odpri povezavo %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Odpri povezavo" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Kopiraj povezavo na odložišče" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Zapri" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Sistem oken %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (izgradnja za %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Zapri" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Nastavitve" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Nastavitve — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Trenutna stran. Napredek: %1 odst." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Krmari proti %1. Napredek: %2 odst." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Trenutna stran." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Krmari proti %1. Zahteva pozornost." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Krmari proti %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Več dejanj" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Obiščite stran %1 v KDE Store" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Kopiraj naslov povezave" + +#~ msgctxt "LoadingPlaceholder|" +#~ msgid "Still loading, please wait." +#~ msgstr "Še vedno nalagam, prosim počakajte." diff --git a/poqm/sr/libkirigami6_qt.po b/poqm/sr/libkirigami6_qt.po new file mode 100644 index 0000000..0ecaf69 --- /dev/null +++ b/poqm/sr/libkirigami6_qt.po @@ -0,0 +1,293 @@ +# Translation of libkirigami2plugin_qt.po into Serbian. +# Chusslove Illich , 2017. +msgid "" +msgstr "" +"Project-Id-Version: libkirigami2plugin_qt\n" +"PO-Revision-Date: 2017-10-06 17:14+0200\n" +"Last-Translator: Chusslove Illich \n" +"Language-Team: Serbian \n" +"Language: sr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: qtrich\n" +"X-Environment: kde\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:196 +#, fuzzy +#| msgctxt "ContextDrawer|" +#| msgid "Actions" +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Радње" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Радње" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Назад" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Иди назад" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Иди напред" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "" + +#, fuzzy +#~| msgctxt "ForwardButton|" +#~| msgid "Navigate Forward" +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Иди напред" + +#, fuzzy +#~| msgctxt "ContextDrawer|" +#~| msgid "Actions" +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Радње" diff --git a/poqm/sr@ijekavian/libkirigami6_qt.po b/poqm/sr@ijekavian/libkirigami6_qt.po new file mode 100644 index 0000000..1885690 --- /dev/null +++ b/poqm/sr@ijekavian/libkirigami6_qt.po @@ -0,0 +1,293 @@ +# Translation of libkirigami2plugin_qt.po into Serbian. +# Chusslove Illich , 2017. +msgid "" +msgstr "" +"Project-Id-Version: libkirigami2plugin_qt\n" +"PO-Revision-Date: 2017-10-06 17:14+0200\n" +"Last-Translator: Chusslove Illich \n" +"Language-Team: Serbian \n" +"Language: sr@ijekavian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: qtrich\n" +"X-Environment: kde\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:196 +#, fuzzy +#| msgctxt "ContextDrawer|" +#| msgid "Actions" +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Радње" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Радње" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Назад" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Иди назад" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Иди напред" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "" + +#, fuzzy +#~| msgctxt "ForwardButton|" +#~| msgid "Navigate Forward" +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Иди напред" + +#, fuzzy +#~| msgctxt "ContextDrawer|" +#~| msgid "Actions" +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Радње" diff --git a/poqm/sr@ijekavianlatin/libkirigami6_qt.po b/poqm/sr@ijekavianlatin/libkirigami6_qt.po new file mode 100644 index 0000000..a27690d --- /dev/null +++ b/poqm/sr@ijekavianlatin/libkirigami6_qt.po @@ -0,0 +1,293 @@ +# Translation of libkirigami2plugin_qt.po into Serbian. +# Chusslove Illich , 2017. +msgid "" +msgstr "" +"Project-Id-Version: libkirigami2plugin_qt\n" +"PO-Revision-Date: 2017-10-06 17:14+0200\n" +"Last-Translator: Chusslove Illich \n" +"Language-Team: Serbian \n" +"Language: sr@ijekavianlatin\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: qtrich\n" +"X-Environment: kde\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:196 +#, fuzzy +#| msgctxt "ContextDrawer|" +#| msgid "Actions" +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Radnje" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Radnje" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Nazad" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Idi nazad" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Idi napred" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "" + +#, fuzzy +#~| msgctxt "ForwardButton|" +#~| msgid "Navigate Forward" +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Idi napred" + +#, fuzzy +#~| msgctxt "ContextDrawer|" +#~| msgid "Actions" +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Radnje" diff --git a/poqm/sr@latin/libkirigami6_qt.po b/poqm/sr@latin/libkirigami6_qt.po new file mode 100644 index 0000000..a82f576 --- /dev/null +++ b/poqm/sr@latin/libkirigami6_qt.po @@ -0,0 +1,293 @@ +# Translation of libkirigami2plugin_qt.po into Serbian. +# Chusslove Illich , 2017. +msgid "" +msgstr "" +"Project-Id-Version: libkirigami2plugin_qt\n" +"PO-Revision-Date: 2017-10-06 17:14+0200\n" +"Last-Translator: Chusslove Illich \n" +"Language-Team: Serbian \n" +"Language: sr@latin\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: qtrich\n" +"X-Environment: kde\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:196 +#, fuzzy +#| msgctxt "ContextDrawer|" +#| msgid "Actions" +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Radnje" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Radnje" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Nazad" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Idi nazad" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Idi napred" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "" + +#, fuzzy +#~| msgctxt "ForwardButton|" +#~| msgid "Navigate Forward" +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Idi napred" + +#, fuzzy +#~| msgctxt "ContextDrawer|" +#~| msgid "Actions" +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Radnje" diff --git a/poqm/sv/libkirigami6_qt.po b/poqm/sv/libkirigami6_qt.po new file mode 100644 index 0000000..68da9d5 --- /dev/null +++ b/poqm/sv/libkirigami6_qt.po @@ -0,0 +1,340 @@ +# SPDX-FileCopyrightText: 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2024, 2025 Stefan Asserhäll +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2025-03-26 17:51+0100\n" +"Last-Translator: Stefan Asserhäll \n" +"Language-Team: Swedish \n" +"Language: sv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 24.12.0\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Skicka e-post till %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Engagera dig" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Ge bidrag" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Rapportera ett fel" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licens:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licens: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Använda bibliotek" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Upphovsmän" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Visa foton av upphovsmän" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Tack till" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Översättare" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Om %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Avsluta" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Fler åtgärder" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Ta bort etikett" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Åtgärder" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Visa sammanhangsberoende hjälp" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Tillbaka" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Stäng sidorad" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Öppna sidorad" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Läser in…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Lösenord" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Dölj lösenord" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Visa lösenord" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Stäng meny" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Visa meny" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Sök…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Sök" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Rensa sökning" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Kopiera" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Markera alla" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Lyckades" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Varning" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Fel" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Notera" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Stäng" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Stäng låda" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Öppna låda" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Stäng" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navigera bakåt" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navigera framåt" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Öppna länk %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Öppna länk" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Kopiera länk till klippbordet" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Stäng" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Ramverk %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Fönsterhanteringssystemet %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (byggt för %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Stäng" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Inställningar" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Inställningar — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Aktuell sida. Förlopp: %1 procent." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Navigera till %1. Förlopp: %2 procent." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Aktuell sida." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Navigera till %1. Kräver uppmärksamhet." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Navigera till %1." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Fler åtgärder" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Besök KDE-butikens sida för %1" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Kopiera länkadress" + +#~ msgctxt "LoadingPlaceholder|" +#~ msgid "Still loading, please wait." +#~ msgstr "Läser fortfarande in, vänta." + +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Sök..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Fler åtgärder" diff --git a/poqm/ta/libkirigami6_qt.po b/poqm/ta/libkirigami6_qt.po new file mode 100644 index 0000000..cfd7646 --- /dev/null +++ b/poqm/ta/libkirigami6_qt.po @@ -0,0 +1,328 @@ +# SPDX-FileCopyrightText: 2021, 2022, 2023, 2024, 2025 Kishore G +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2025-03-16 14:48+0530\n" +"Last-Translator: Kishore G \n" +"Language-Team: Tamil \n" +"Language: ta\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 24.12.3\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "%1 என்பவருக்கு மின்னஞ்சல் அனுப்பு" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "பங்களித்தல்" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "நன்கொடை அளி" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "பிழையை தெரிவி" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "பதிப்புரிமை" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "உரிமம்:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "உரிமம்: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "பயன்படுத்தும் நிரலகங்கள்" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "இயற்றியவர்கள்" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "இயற்றியவர்களின் படங்களைக் காட்டு" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "நன்றி" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "மொழிபெயர்ப்பாளர்கள்" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1 பற்றி " + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "வெளியேறு" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "மேலும் செயல்கள்" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "குறிச்சொல்லை நீக்கு" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "செயல்கள்" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "சூழல்சார் உதவியைக் காட்டு" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "பின்னே" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "ஓரப்பட்டையை மூடு" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "ஓரப்பட்டையை திற" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "ஏற்றப்படுகிறது…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "கடவுச்சொல்" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "கடவுச்சொல்லை மறை" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "கடவுச்சொல்லை காட்டு" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "பட்டியை மூடு" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "பட்டியைத் திற" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "தேடு..." + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "தேடல்" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "தேடலை காலியாக்கு" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "நகலெடு" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "அனைத்தையும் தேர்ந்தெடு" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "வெற்றி" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "எச்சரிக்கை" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "சிக்கல்" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "குறிப்பு" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "மூடு" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "மேல்தோன்றும் பலகையை மூடு" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "மேல்தோன்றும் பலகையைத் திற" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "மூடு" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "பின்னே செல்" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "முன்னே செல்" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "%1 இணைப்பைத் திற" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "இணைப்பைத் திற" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "இணைப்பை பிடிப்புப்பலகைக்கு நகலெடு" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "மூடு" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "கே.டீ.யீ. நிரலகங்கள் %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "%1 சாளர நெறிமுறை" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (%3 கொண்டு தொகுக்கப்பட்டது)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "மூடு" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "அமைப்புகள்" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "அமைப்புகள் — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "தற்போதைய பக்கம். முன்னேற்றம்: %1 சதவீதம்." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "%1-க்கு செல். முன்னேற்றம்: %2 சதவீதம்." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "தற்போதைய பக்கம்" + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "%1-க்கு செல். கவனத்தை கோருகிறது." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "%1-க்கு செல்." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "மேலும் செயல்கள்" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "%1-இன் கே.டீ.யீ. கடைவீதி பக்கத்தை பாருங்கள்" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "இணைப்பின் முகவரியை நகலெடு" + +#~ msgctxt "LoadingPlaceholder|" +#~ msgid "Still loading, please wait." +#~ msgstr "இன்னும் ஏற்றப்படுகிறது, காத்திருங்கள்." + +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" diff --git a/poqm/tg/libkirigami6_qt.po b/poqm/tg/libkirigami6_qt.po new file mode 100644 index 0000000..27e5650 --- /dev/null +++ b/poqm/tg/libkirigami6_qt.po @@ -0,0 +1,339 @@ +# Victor Ibragimov , 2019. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2019-08-16 20:02+0500\n" +"Last-Translator: Victor Ibragimov \n" +"Language-Team: English \n" +"Language: tg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 19.04.3\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "" + +#: controls/AboutItem.qml:258 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Ҳуқуқи муаллиф" + +#: controls/AboutItem.qml:302 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "License:" +msgctxt "AboutItem|" +msgid "License:" +msgstr "Иҷозатнома:" + +#: controls/AboutItem.qml:324 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "License: %1" +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Иҷозатнома: %1" + +#: controls/AboutItem.qml:335 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Libraries in use" +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Китобхонаҳое, ки истифода мешаванд" + +#: controls/AboutItem.qml:365 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Authors" +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Муаллифон" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:386 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Credits" +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Сипосгузорӣ" + +#: controls/AboutItem.qml:398 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Translators" +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Тарҷумонон" + +#: controls/AboutPage.qml:100 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "About" +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Дар бораи барнома" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:196 +#, fuzzy +#| msgctxt "ToolBarApplicationHeader|" +#| msgid "More Actions" +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Амалҳои бештар" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Амалҳо" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Ба қафо" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Ниҳонвожа" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Ниҳонвожа" + +#: controls/PasswordField.qml:45 +#, fuzzy +#| msgctxt "PasswordField|" +#| msgid "Password" +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Ниҳонвожа" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "" + +#: controls/SearchField.qml:86 +#, fuzzy +#| msgctxt "SearchField|" +#| msgid "Search..." +msgctxt "SearchField|" +msgid "Search…" +msgstr "Ҷустуҷӯ..." + +#: controls/SearchField.qml:88 +#, fuzzy +#| msgctxt "SearchField|" +#| msgid "Search..." +msgctxt "SearchField|" +msgid "Search" +msgstr "Ҷустуҷӯ..." + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "" + +#: controls/SelectableLabel.qml:179 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Ҳуқуқи муаллиф" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Ба қафо паймудан" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Ба пеш паймудан" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "" + +#, fuzzy +#~| msgctxt "ForwardButton|" +#~| msgid "Navigate Forward" +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Ба пеш паймудан" + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Амалҳои бештар" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Нусха бардоштани нишонии пайванд" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Ҷустуҷӯ..." + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Амалҳои бештар" diff --git a/poqm/tr/libkirigami6_qt.po b/poqm/tr/libkirigami6_qt.po new file mode 100644 index 0000000..3831aee --- /dev/null +++ b/poqm/tr/libkirigami6_qt.po @@ -0,0 +1,278 @@ +# Volkan Gezer , 2021. +# SPDX-FileCopyrightText: 2022, 2023, 2024, 2025 Emir SARI +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2025-03-12 12:36+0300\n" +"Last-Translator: Emir SARI \n" +"Language-Team: Turkish \n" +"Language: tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Lokalize 25.07.70\n" +"X-Qt-Contexts: true\n" +"X-POOTLE-MTIME: 1497529432.000000\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "%1 kişisine bir e-posta gönder" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Katıl" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Bağış Yap" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Hata Bildir" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Telif Hakkı" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Lisans:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Lisans: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Kullanımdaki Kitaplıklar" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Yazarlar" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Yazar fotoğraflarını göster" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Emeği Geçenler" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Çevirmenler" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1 Hakkında" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Çık" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Daha fazla eylem" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Etiketi Kaldır" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Eylemler" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Bağlamsal Yardımı Göster" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Geri" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Kenar çubuğunu kapat" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Kenar çubuğunu aç" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Yükleniyor…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Parola" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Parolayı Gizle" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Parolayı Göster" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Menüyü kapat" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Menüyü aç" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Ara…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Ara" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Aramayı temizle" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Kopyala" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Tümünü Seç" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Başarılı" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Uyarı" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Hata" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Not" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Kapat" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Çekmeceyi kapat" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Çekmeceyi aç" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Kapat" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Geri Git" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "İleri Git" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "%1 bağlantısını aç" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Bağlantıyı aç" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Bağlantıyı Panoya Kopyala" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Kapat" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "%1 pencereleme sistemi" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (%3 üzerine yapılı)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Kapat" diff --git a/poqm/uk/libkirigami6_qt.po b/poqm/uk/libkirigami6_qt.po new file mode 100644 index 0000000..d373e49 --- /dev/null +++ b/poqm/uk/libkirigami6_qt.po @@ -0,0 +1,347 @@ +# Translation of libkirigami2plugin_qt.po to Ukrainian +# Copyright (C) 2016-2021 This_file_is_part_of_KDE +# This file is distributed under the license LGPL version 2.1 or +# version 3 or later versions approved by the membership of KDE e.V. +# +# Yuri Chornoivan , 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025. +msgid "" +msgstr "" +"Project-Id-Version: libkirigami2plugin_qt\n" +"PO-Revision-Date: 2025-03-12 11:17+0200\n" +"Last-Translator: Yuri Chornoivan \n" +"Language-Team: Ukrainian \n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=4; plural=n==1 ? 3 : n%10==1 && n%100!=11 ? 0 : n" +"%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Lokalize 23.04.3\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Надіслати повідомлення ел. пошти до %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Участь у команді" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "Підтримати фінансово" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "Повідомити про ваду" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Авторські права" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Ліцензування:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Ліцензування: %1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Використані бібліотеки" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Автори" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Показати фотографії авторів" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Подяки" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Перекладачі" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Про %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Вийти" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Інші дії" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Вилучити мітку" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Дії" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "Показати контекстну довідку" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Назад" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Закрити бічну панель" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Відкрити бічну панель" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Завантаження…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Пароль" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "Приховати пароль" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "Показати пароль" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "Закрити меню" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "Відкрити меню" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Шукати…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "Пошук" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "Спорожнити поле пошуку" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "Копіювати" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "Вибрати все" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "Успіх" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "Попередження" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "Помилка" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "Нотатка" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "Закрити" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Закрити висувну панель" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Відкрити висувну панель" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "Закрити" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Перейти назад" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Перейти далі" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "Відкрити посилання %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "Відкрити посилання" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Копіювати посилання до буфера" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "Закрити" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "Система керування вікнами %1" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (зібрано з використанням %3)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "Закрити" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "Параметри" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "Параметри — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "Поточна сторінка. Поступ: %1 відсотків." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "Перехід до %1. Поступ: %2 відсотків." + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "Поточна сторінка." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "Перейти до %1. Вимагає уваги." + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "Перейти до «%1»." + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "Інші дії" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "Відвідайте сторінку %1 у KDE Store" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "Копіювати адресу посилання" + +#~ msgctxt "LoadingPlaceholder|" +#~ msgid "Still loading, please wait." +#~ msgstr "Завантажуємо. Будь ласка, зачекайте." + +#~ msgctxt "AboutItem|" +#~ msgid "(%1)" +#~ msgstr "(%1)" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "Шукати…" + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "Інші дії" diff --git a/poqm/zh_CN/libkirigami6_qt.po b/poqm/zh_CN/libkirigami6_qt.po new file mode 100644 index 0000000..d6a1f0f --- /dev/null +++ b/poqm/zh_CN/libkirigami6_qt.po @@ -0,0 +1,274 @@ +msgid "" +msgstr "" +"Project-Id-Version: kdeorg\n" +"PO-Revision-Date: 2024-04-22 15:58\n" +"Language-Team: Chinese Simplified\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: kdeorg\n" +"X-Crowdin-Project-ID: 269464\n" +"X-Crowdin-Language: zh-CN\n" +"X-Crowdin-File: /kf6-trunk/messages/kirigami/libkirigami6_qt.pot\n" +"X-Crowdin-File-ID: 42919\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "发送电子邮件到 %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "参与贡献" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "捐款" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "报告程序缺陷" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "版权信息" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "许可证:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "许可证:%1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "使用的程序库" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "作者" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "显示作者照片" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "致谢" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "翻译人员" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "关于 %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "退出" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "更多操作" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "移除标签" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "操作" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "显示相关帮助" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "返回" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "隐藏侧边栏" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "显示侧边栏" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "正在加载…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "密码" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "隐藏密码" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "显示密码" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "关闭菜单" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "打开菜单" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "搜索…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "搜索" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "清除搜索内容" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "复制" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "全部选择" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "成功" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "警告" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "错误" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "注意" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "关闭" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "关闭抽屉栏" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "打开抽屉栏" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "关闭" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "后退" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "前进" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "打开链接 %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "打开链接" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "复制链接至剪贴板" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "关闭" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE 程序框架 %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "%1 窗口系统" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (使用 %3 构建)" diff --git a/poqm/zh_TW/libkirigami6_qt.po b/poqm/zh_TW/libkirigami6_qt.po new file mode 100644 index 0000000..e422481 --- /dev/null +++ b/poqm/zh_TW/libkirigami6_qt.po @@ -0,0 +1,338 @@ +# SPDX-FileCopyrightText: 2022, 2023, 2024, 2025 Kisaragi Hiu +# Jeff Huang , 2016. +# Franklin Weng , 2017. +# pan93412 , 2018, 2019. +# Kisaragi Hiu , 2023. +msgid "" +msgstr "" +"Project-Id-Version: libkirigamiplugin_qt\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-09-22 20:41+0800\n" +"PO-Revision-Date: 2025-03-26 18:06+0900\n" +"Last-Translator: Kisaragi Hiu \n" +"Language-Team: Traditional Chinese \n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Lokalize 24.12.3\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:163 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1(%2)" + +#: controls/AboutItem.qml:172 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "傳送電子郵件給 %1" + +#: controls/AboutItem.qml:222 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "參與" + +#: controls/AboutItem.qml:228 +msgctxt "AboutItem|" +msgid "Donate" +msgstr "贊助" + +#: controls/AboutItem.qml:245 +msgctxt "AboutItem|" +msgid "Report a Bug" +msgstr "回報問題" + +#: controls/AboutItem.qml:258 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "著作權" + +#: controls/AboutItem.qml:302 +msgctxt "AboutItem|" +msgid "License:" +msgstr "授權條款:" + +#: controls/AboutItem.qml:324 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "授權條款:%1" + +#: controls/AboutItem.qml:335 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "使用函式庫" + +#: controls/AboutItem.qml:365 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "作者群" + +#: controls/AboutItem.qml:375 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "顯示作者照片" + +#: controls/AboutItem.qml:386 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "致謝" + +#: controls/AboutItem.qml:398 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "翻譯者" + +#: controls/AboutPage.qml:100 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "關於 %1" + +#: controls/AbstractApplicationWindow.qml:176 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "離開" + +#: controls/ActionToolBar.qml:196 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "更多動作" + +#: controls/Chip.qml:86 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "移除標籤" + +#: controls/ContextDrawer.qml:59 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "動作" + +#: controls/ContextualHelpButton.qml:50 +msgctxt "ContextualHelpButton|" +msgid "Show Contextual Help" +msgstr "顯示內文說明" + +#: controls/GlobalDrawer.qml:346 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "返回" + +#: controls/GlobalDrawer.qml:661 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "關閉側邊欄" + +#: controls/GlobalDrawer.qml:666 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "開啟側邊欄" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "載入中…" + +#: controls/PasswordField.qml:42 +msgctxt "PasswordField|" +msgid "Password" +msgstr "密碼" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Hide Password" +msgstr "隱藏密碼" + +#: controls/PasswordField.qml:45 +msgctxt "PasswordField|" +msgid "Show Password" +msgstr "顯示密碼" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Close menu" +msgstr "關閉選單" + +#: controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml:83 +msgctxt "PageRowGlobalToolBarUI|" +msgid "Open menu" +msgstr "開啟選單" + +#: controls/SearchField.qml:86 +msgctxt "SearchField|" +msgid "Search…" +msgstr "搜尋…" + +#: controls/SearchField.qml:88 +msgctxt "SearchField|" +msgid "Search" +msgstr "搜尋" + +#: controls/SearchField.qml:99 +msgctxt "SearchField|" +msgid "Clear search" +msgstr "清除搜尋" + +#: controls/SelectableLabel.qml:179 +msgctxt "SelectableLabel|" +msgid "Copy" +msgstr "複製" + +#: controls/SelectableLabel.qml:192 +msgctxt "SelectableLabel|" +msgid "Select All" +msgstr "全部選取" + +#: controls/templates/InlineMessage.qml:288 +msgctxt "InlineMessage|" +msgid "Success" +msgstr "成功" + +#: controls/templates/InlineMessage.qml:290 +msgctxt "InlineMessage|" +msgid "Warning" +msgstr "警告" + +#: controls/templates/InlineMessage.qml:292 +msgctxt "InlineMessage|" +msgid "Error" +msgstr "錯誤" + +#: controls/templates/InlineMessage.qml:294 +msgctxt "InlineMessage|" +msgid "Note" +msgstr "備註" + +#: controls/templates/InlineMessage.qml:400 +msgctxt "InlineMessage|" +msgid "Close" +msgstr "關閉" + +#: controls/templates/OverlayDrawer.qml:128 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "關閉抽屜" + +#: controls/templates/OverlayDrawer.qml:134 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "開啟抽屜" + +#: controls/templates/OverlaySheet.qml:290 +msgctxt "OverlaySheet|@action:button close dialog" +msgid "Close" +msgstr "關閉" + +#: controls/templates/private/BackButton.qml:50 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "返回" + +#: controls/templates/private/ForwardButton.qml:27 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "往前" + +#: controls/UrlButton.qml:47 +#, qt-format +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link %1" +msgstr "開啟連結 %1" + +#: controls/UrlButton.qml:48 +msgctxt "UrlButton|@info:whatsthis" +msgid "Open link" +msgstr "開啟連結" + +#: controls/UrlButton.qml:90 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "複製連結至剪貼簿" + +#: dialogs/DialogHeaderTopContent.qml:89 +msgctxt "DialogHeaderTopContent|@action:button close dialog" +msgid "Close" +msgstr "關閉" + +#: platform/settings.cpp:219 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: platform/settings.cpp:221 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "The %1 windowing system" +msgstr "%1 視窗系統" + +#: platform/settings.cpp:222 +#, qt-format +msgctxt "Kirigami::Platform::Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (建置於 %3 上)" + +#~ msgctxt "Dialog|@action:button close dialog" +#~ msgid "Close" +#~ msgstr "關閉" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings" +#~ msgstr "設定" + +#~ msgctxt "CategorizedSettings|" +#~ msgid "Settings — %1" +#~ msgstr "設定 — %1" + +#~ msgctxt "Avatar|" +#~ msgid "%1 — %2" +#~ msgstr "%1 — %2" + +#~ msgctxt "PageTab|" +#~ msgid "Current page. Progress: %1 percent." +#~ msgstr "目前頁面。進度:百分之 %1。" + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Progress: %2 percent." +#~ msgstr "前往 %1。進度:百分之 %2。" + +#~ msgctxt "PageTab|" +#~ msgid "Current page." +#~ msgstr "目前頁面。" + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1. Demanding attention." +#~ msgstr "前往 %1。正在請求注意。" + +#~ msgctxt "PageTab|" +#~ msgid "Navigate to %1." +#~ msgstr "前往 %1。" + +#~ msgctxt "ToolBarApplicationHeader|" +#~ msgid "More Actions" +#~ msgstr "更多動作" + +#~ msgctxt "AboutItem|" +#~ msgid "Visit %1's KDE Store page" +#~ msgstr "造訪 %1 在 KDE Store 的頁面" + +#~ msgctxt "UrlButton|" +#~ msgid "Copy link address" +#~ msgstr "複製連結網址" + +#~ msgctxt "SearchField|" +#~ msgid "Search..." +#~ msgstr "搜尋…" + +#~ msgctxt "AboutPage|" +#~ msgid "%1 <%2>" +#~ msgstr "%1 <%2>" + +#~ msgctxt "ToolBarPageHeader|" +#~ msgid "More Actions" +#~ msgstr "更多動作" diff --git a/qmllint.ini.in b/qmllint.ini.in new file mode 100644 index 0000000..87a5e87 --- /dev/null +++ b/qmllint.ini.in @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2024 Carl Schwan +# SPDX-License-Identifier: CC0-1.0 + +[General] +DisableDefaultImports=false +AdditionalQmlImportPaths=${QT_QML_OUTPUT_DIRECTORY} + +[Warnings] +AccessSingletonViaObject=warning +AttachedPropertyReuse=disable +BadSignalHandlerParameters=warning +CompilerWarnings=disable +Deprecated=warning +DuplicatePropertyBinding=warning +DuplicatedName=warning +ImportFailure=warning +IncompatibleType=warning +InheritanceCycle=warning +InvalidLintDirective=warning +LintPluginWarnings=disable +MissingProperty=warning +MissingType=warning +MultilineStrings=info +NonListProperty=warning +PrefixedImportType=warning +PropertyAliasCycles=warning +ReadOnlyProperty=warning +RequiredProperty=warning +RestrictedType=warning +TopLevelComponent=warning +UncreatableType=warning +UnqualifiedAccess=warning +UnresolvedType=warning +UnusedImports=warning +UseProperFunction=warning +VarUsedBeforeDeclaration=warning +WithStatement=warning diff --git a/scripts/gen_icons_qrc.sh b/scripts/gen_icons_qrc.sh new file mode 100755 index 0000000..e8850a3 --- /dev/null +++ b/scripts/gen_icons_qrc.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +SRC_DIR="src/" +BREEZEICONS_DIR="breeze-icons" +ICONS_SIZES=(48 32 22) +TAB=" " + +kirigami_dir="$(cd $(dirname $(readlink -f $0))/.. && pwd)" + +case $1 in +-h|--help) + echo "usage: $(basename $0)" + exit 1 + ;; +esac + +if [[ ! -d ${kirigami_dir}/${BREEZEICONS_DIR} ]]; then + echo "could not find ${BREEZEICONS_DIR}, please clone breeze-icons first into ${BREEZEICONS_DIR}:" + echo "cd ${kirigami_dir} && git clone --depth 1 https://invent.kde.org/frameworks/breeze-icons.git ${BREEZEICONS_DIR}" + exit 1 +fi + +pushd ${kirigami_dir} > /dev/null + +# find strings associated to variable with 'icon' in name and put them into an array +if [[ -n $(which ag 2>/dev/null) ]]; then + possible_icons=($(ag --ignore Icon.qml --file-search-regex "\.qml" --only-matching --nonumbers --noheading --nofilename "icon.*\".+\"" ${SRC_DIR} | egrep -o "*\".+\"")) + # try to find in Icon { ... source: "xyz" ... } + possible_icons+=($(ag --ignore Icon.qml --file-search-regex "\.qml" -A 15 "Icon\s*{" ${SRC_DIR} | egrep "source:" | egrep -o "*\".+\"")) +else + possible_icons=($(find ${SRC_DIR} -name "*.qml" -and -not -name "Icon.qml" -exec egrep "icon.*\".+\"" {} \; | egrep -o "*\".+\"")) +fi + +# sort array and filter out all entry which are not a string ("...") +IFS=$'\n' icons=($(sort -u <<<"${possible_icons[*]}" | egrep -o "*\".+\"" | sed 's/\"//g')) +unset IFS + +#printf "%s\n" "${icons[@]}" + +# generate .qrc +echo "" +echo "${TAB}" + +for icon in ${icons[@]}; do + for size in ${ICONS_SIZES[@]}; do + file=$(find breeze-icons/icons/*/${size}/ -name "${icon}.*" -print -quit) + + if [[ -n ${file} ]]; then + echo -e "${TAB}${TAB}${file}" + #echo ${file} + break + fi + done +done + +echo "${TAB}" +echo "" + +popd > /dev/null diff --git a/scripts/gen_qmltypes.sh b/scripts/gen_qmltypes.sh new file mode 100755 index 0000000..8606f99 --- /dev/null +++ b/scripts/gen_qmltypes.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +QMLPLUGINDUMP=${QMLPLUGINDUMP-qmlplugindump} + +case $1 in +-h|--help) + echo "usage: $(basename $0) IMPORT_PATH" + echo "it uses either '$(which qmlplugindump)' or the one set by 'QMLPLUGINDUMP'" + exit 1 + ;; +esac + +[[ -z ${1} ]] && { echo "no import path not given, exit"; exit 1; } + +echo "using '${QMLPLUGINDUMP}' as dump tool" >&2 + +${QMLPLUGINDUMP} -noinstantiate -notrelocatable -platform xcb org.kde.kirigami 2.0 "${1}" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..56e7ac3 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,322 @@ +add_subdirectory(platform) +add_subdirectory(primitives) +add_subdirectory(delegates) +add_subdirectory(dialogs) +add_subdirectory(layouts) + +add_library(Kirigami) +add_library(KF6::Kirigami ALIAS Kirigami) + +# On Windows Kirigami apparently adds too many sources so on Windows we end +# up running into command line length limits. So disable cache +# generation on Windows for now. +# On Qt 6.7.2 cachegen is causing https://bugs.kde.org/show_bug.cgi?id=488326 +# investigate if future versions fix it and we can re-enable it +if (NOT ANDROID) + set(_extra_options NO_CACHEGEN) +endif() +if (BUILD_SHARED_LIBS) + set(_extra_options ${_extra_options} NO_PLUGIN_OPTIONAL) +endif() +if (ANDROID OR NOT BUILD_SHARED_LIBS) + set(_extra_options ${_extra_options} OPTIONAL_IMPORTS org.kde.breeze) +endif() + +# Module: org.kde.kirigami.private + +add_library(KirigamiPrivate) +ecm_add_qml_module(KirigamiPrivate + URI "org.kde.kirigami.private" + GENERATE_PLUGIN_SOURCE + INSTALLED_PLUGIN_TARGET KF6KirigamiPrivateplugin +) + +set_target_properties(KirigamiPrivate PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION 6 + EXPORT_NAME "KirigamiPrivate" +) + +target_sources(KirigamiPrivate PRIVATE + copyhelper.cpp + copyhelper.h + actionhelper.cpp + actionhelper.h +) + +target_link_libraries(KirigamiPrivate PRIVATE Qt6::Gui) + +ecm_finalize_qml_module(KirigamiPrivate DESTINATION ${KDE_INSTALL_QMLDIR} EXPORT KirigamiTargets) + +install(TARGETS KirigamiPrivate EXPORT KirigamiTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS}) + +# Module: org.kde.kirigami + +ecm_add_qml_module(Kirigami URI "org.kde.kirigami" VERSION 2.0 + CLASS_NAME KirigamiPlugin + INSTALLED_PLUGIN_TARGET KF6Kirigamiplugin + DEPENDENCIES + "QtQuick.Controls" + "org.kde.kirigami.private" + IMPORTS + "org.kde.kirigami.platform" + "org.kde.kirigami.primitives" + "org.kde.kirigami.delegates" + "org.kde.kirigami.dialogs" + "org.kde.kirigami.layouts" + ${_extra_options} +) + +ecm_create_qm_loader(kirigami_QM_LOADER libkirigami6_qt) +target_sources(Kirigami PRIVATE ${kirigami_QM_LOADER}) + +ecm_qt_declare_logging_category(Kirigami + HEADER loggingcategory.h + IDENTIFIER KirigamiLog + CATEGORY_NAME kf.kirigami + DESCRIPTION "Kirigami" + DEFAULT_SEVERITY Warning + EXPORT KIRIGAMI +) + +set_target_properties(Kirigami PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION 6 + EXPORT_NAME "Kirigami" +) + +target_include_directories(Kirigami PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/platform + ${CMAKE_CURRENT_BINARY_DIR}/platform +) + +target_sources(Kirigami PRIVATE + enums.h + imagecolors.cpp + imagecolors.h + mnemonicattached.cpp + mnemonicattached.h + overlayzstackingattached.cpp + overlayzstackingattached.h + pagepool.cpp + pagepool.h + scenepositionattached.cpp + scenepositionattached.h + spellcheckattached.cpp + spellcheckattached.h + wheelhandler.cpp + wheelhandler.h +) + +target_sources(Kirigamiplugin PRIVATE + kirigamiplugin.cpp + kirigamiplugin.h +) + +ecm_target_qml_sources(Kirigami SOURCES + controls/Action.qml + controls/AbstractApplicationHeader.qml + controls/AbstractApplicationWindow.qml + controls/ApplicationWindow.qml + controls/OverlayDrawer.qml + controls/ContextDrawer.qml + controls/GlobalDrawer.qml + controls/Heading.qml + controls/PageRow.qml + controls/OverlaySheet.qml + controls/Page.qml + controls/ScrollablePage.qml + controls/SwipeListItem.qml +) + +ecm_target_qml_sources(Kirigami VERSION 2.1 SOURCES + controls/AbstractApplicationItem.qml + controls/ApplicationItem.qml +) + +ecm_target_qml_sources(Kirigami VERSION 2.4 SOURCES + controls/AbstractCard.qml + controls/Card.qml + controls/CardsListView.qml + controls/CardsLayout.qml + controls/InlineMessage.qml +) + +ecm_target_qml_sources(Kirigami VERSION 2.5 SOURCES + controls/ListItemDragHandle.qml + controls/ActionToolBar.qml +) + +ecm_target_qml_sources(Kirigami VERSION 2.6 SOURCES + controls/AboutPage.qml + controls/LinkButton.qml + controls/UrlButton.qml +) + +ecm_target_qml_sources(Kirigami VERSION 2.7 SOURCES + controls/ActionTextField.qml +) + +ecm_target_qml_sources(Kirigami VERSION 2.8 SOURCES + controls/SearchField.qml + controls/PasswordField.qml +) + +ecm_target_qml_sources(Kirigami VERSION 2.10 SOURCES + controls/ListSectionHeader.qml +) + +ecm_target_qml_sources(Kirigami VERSION 2.11 SOURCES + controls/PagePoolAction.qml +) + +ecm_target_qml_sources(Kirigami VERSION 2.12 SOURCES + controls/PlaceholderMessage.qml +) + +ecm_target_qml_sources(Kirigami VERSION 2.14 SOURCES + controls/FlexColumn.qml +) + +ecm_target_qml_sources(Kirigami VERSION 2.19 SOURCES + controls/AboutItem.qml + controls/NavigationTabBar.qml + controls/NavigationTabButton.qml + controls/Chip.qml + controls/LoadingPlaceholder.qml +) + +ecm_target_qml_sources(Kirigami VERSION 2.20 SOURCES + controls/SelectableLabel.qml + controls/InlineViewHeader.qml + controls/ContextualHelpButton.qml +) + +ecm_target_qml_sources(Kirigami PRIVATE PATH private SOURCES + controls/private/ActionIconGroup.qml + controls/private/ActionMenuItem.qml + controls/private/ActionsMenu.qml + controls/private/BannerImage.qml + controls/private/ContextDrawerActionItem.qml + controls/private/DefaultCardBackground.qml + controls/private/DefaultChipBackground.qml + controls/private/DefaultPageTitleDelegate.qml + controls/private/EdgeShadow.qml + controls/private/GlobalDrawerActionItem.qml + controls/private/MobileDialogLayer.qml + controls/private/PrivateActionToolButton.qml + controls/private/PullDownIndicator.qml + controls/private/SwipeItemEventFilter.qml +) + +ecm_target_qml_sources(Kirigami PRIVATE PATH private/globaltoolbar SOURCES + controls/private/globaltoolbar/AbstractPageHeader.qml + controls/private/globaltoolbar/BreadcrumbControl.qml + controls/private/globaltoolbar/PageRowGlobalToolBarStyleGroup.qml + controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml + controls/private/globaltoolbar/TitlesPageHeader.qml + controls/private/globaltoolbar/ToolBarPageHeader.qml + controls/private/globaltoolbar/ToolBarPageFooter.qml +) + +ecm_target_qml_sources(Kirigami PRIVATE PATH templates SOURCES + controls/templates/AbstractApplicationHeader.qml + controls/templates/AbstractCard.qml + controls/templates/Chip.qml + controls/templates/InlineMessage.qml + controls/templates/OverlayDrawer.qml + controls/templates/OverlaySheet.qml + controls/templates/SingletonHeaderSizeGroup.qml + controls/templates/qmldir +) + +ecm_target_qml_sources(Kirigami PRIVATE PATH templates/private SOURCES + controls/templates/private/BackButton.qml + controls/templates/private/BorderPropertiesGroup.qml + controls/templates/private/ContextIcon.qml + controls/templates/private/DrawerHandle.qml + controls/templates/private/ForwardButton.qml + controls/templates/private/GenericDrawerIcon.qml + controls/templates/private/IconPropertiesGroup.qml + controls/templates/private/MenuIcon.qml + controls/templates/private/PassiveNotificationsManager.qml + controls/templates/private/qmldir +) + +qt_target_qml_sources(Kirigami RESOURCES + styles/Material/InlineMessage.qml + styles/Material/Theme.qml + OUTPUT_TARGETS _out_targets_1 +) + +if (DESKTOP_ENABLED) + qt_target_qml_sources(Kirigami RESOURCES + styles/org.kde.desktop/AbstractApplicationHeader.qml + styles/org.kde.desktop/Theme.qml + OUTPUT_TARGETS _out_targets_2 + ) +endif() + +target_link_libraries(Kirigami + PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Qml + Qt6::Quick + PRIVATE + Qt6::Concurrent + ${Kirigami_EXTRA_LIBS} +) + +if (HAVE_OpenMP) + target_link_libraries(Kirigami PRIVATE OpenMP::OpenMP_CXX) +endif() + +if (NOT BUILD_SHARED_LIBS) + # Ensure we install the plugin library file as that's required to link + # against for static builds to work properly + target_link_libraries(Kirigamiplugin + PRIVATE + KirigamiPlatformplugin + KirigamiDelegatesplugin + KirigamiPrimitivesplugin + KirigamiDialogsplugin + KirigamiLayoutsplugin + KirigamiPrivateplugin + ) + # for tests to find this under the name it's actually installed with' + add_library(KF6Kirigamiplugin ALIAS Kirigamiplugin) +else() + target_link_libraries(Kirigami + PUBLIC + KirigamiPlatform + PRIVATE + KirigamiDelegates + KirigamiPrimitives + KirigamiDialogs + KirigamiLayouts + KirigamiPrivate + ) +endif() + +install(TARGETS Kirigami ${_out_targets_1} ${_out_targets_2} EXPORT KirigamiTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS}) +install(EXPORT KirigamiTargets + DESTINATION ${KDE_INSTALL_CMAKEPACKAGEDIR}/KF6Kirigami + FILE KF6KirigamiTargets.cmake + NAMESPACE KF6 +) + +ecm_finalize_qml_module(Kirigami DESTINATION ${KDE_INSTALL_QMLDIR} EXPORT KirigamiTargets) + +if (ANDROID) + install(FILES Kirigami-android-dependencies.xml + DESTINATION ${KDE_INSTALL_LIBDIR} + RENAME Kirigami_${CMAKE_ANDROID_ARCH_ABI}-android-dependencies.xml + ) +endif() + +ecm_qt_install_logging_categories( + EXPORT KIRIGAMI + FILE kirigami.categories + DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR} +) diff --git a/src/Kirigami-android-dependencies.xml b/src/Kirigami-android-dependencies.xml new file mode 100644 index 0000000..ba8fbb4 --- /dev/null +++ b/src/Kirigami-android-dependencies.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/Messages.sh b/src/Messages.sh new file mode 100644 index 0000000..c57f553 --- /dev/null +++ b/src/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACT_TR_STRINGS `find . -name \*.qml -o -name \*.cpp` -o $podir/libkirigami6_qt.pot +rm -f rc.cpp + diff --git a/src/actionhelper.cpp b/src/actionhelper.cpp new file mode 100644 index 0000000..c537298 --- /dev/null +++ b/src/actionhelper.cpp @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2024 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "actionhelper.h" + +ActionHelper::ActionHelper(QObject *parent) + : QObject(parent) +{ +} + +QString ActionHelper::iconName(const QIcon &icon) const +{ + return icon.name(); +} + +QList ActionHelper::alternateShortcuts(QAction *action) const +{ + if (!action || action->shortcuts().length() <= 1) { + return {}; + } else { + return action->shortcuts().mid(1); + } +} + +#include "moc_actionhelper.cpp" diff --git a/src/actionhelper.h b/src/actionhelper.h new file mode 100644 index 0000000..e993b2e --- /dev/null +++ b/src/actionhelper.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include + +/// \internal This is private API, do not use. +class ActionHelper : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + explicit ActionHelper(QObject *parent = nullptr); + + Q_INVOKABLE QList alternateShortcuts(QAction *action) const; + Q_INVOKABLE QString iconName(const QIcon &icon) const; +}; diff --git a/src/controls/AboutItem.qml b/src/controls/AboutItem.qml new file mode 100644 index 0000000..177f396 --- /dev/null +++ b/src/controls/AboutItem.qml @@ -0,0 +1,408 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +//TODO: Kf6: move somewhere else which can depend from KAboutData? +/** + * @brief An about item that displays the about data + * + * Allows to show the copyright notice of the application + * together with the contributors and some information of which platform it's + * running on. + * + * @since 5.87 + * @since org.kde.kirigami 2.19 + */ +Item { + id: aboutItem + /** + * @brief This property holds an object with the same shape as KAboutData. + * + * Example usage: + * @code{json} + * aboutData: { + "displayName" : "KirigamiApp", + "productName" : "kirigami/app", + "componentName" : "kirigamiapp", + "shortDescription" : "A Kirigami example", + "homepage" : "", + "bugAddress" : "submit@bugs.kde.org", + "version" : "5.14.80", + "otherText" : "", + "authors" : [ + { + "name" : "...", + "task" : "", + "emailAddress" : "somebody@kde.org", + "webAddress" : "", + "ocsUsername" : "" + } + ], + "credits" : [], + "translators" : [], + "licenses" : [ + { + "name" : "GPL v2", + "text" : "long, boring, license text", + "spdx" : "GPL-2.0" + } + ], + "copyrightStatement" : "© 2010-2018 Plasma Development Team", + "desktopFileName" : "org.kde.kirigamiapp" + } + @endcode + * + * @see KAboutData + */ + property var aboutData + + /** + * @brief This property holds a link to a "Get Involved" page. + * + * default: `"https://community.kde.org/Get_Involved" when application id starts with "org.kde.", otherwise it is empty.` + */ + property url getInvolvedUrl: aboutData.desktopFileName.startsWith("org.kde.") ? "https://community.kde.org/Get_Involved" : "" + + /** + * @brief This property holds a link to a "Donate" page. + * + * default: `"https://kde.org/community/donations" when application id starts with "org.kde.", otherwise it is empty.` + */ + property url donateUrl: aboutData.desktopFileName.startsWith("org.kde.") ? "https://kde.org/community/donations" : "" + + /** @internal */ + property bool _usePageStack: false + + /** + * @see org::kde::kirigami::FormLayout::wideMode + * @property bool wideMode + */ + property alias wideMode: form.wideMode + + /** @internal */ + default property alias _content: form.data + + // if aboutData is a native KAboutData object, avatarUrl should be a proper url instance, + // otherwise if it was defined as a string in pure JavaScript it should work too. + readonly property bool __hasAvatars: aboutItem.aboutData.authors.some(__hasAvatar) + + function __hasAvatar(person): bool { + return typeof person.avatarUrl !== "undefined" + && person.avatarUrl.toString().length > 0; + } + + /** + * @brief This property controls whether to load avatars by URL. + * + * If set to false, a fallback "user" icon will be displayed. + * + * default: ``false`` + */ + property bool loadAvatars: false + + implicitHeight: form.implicitHeight + implicitWidth: form.implicitWidth + + Component { + id: personDelegate + + RowLayout { + id: delegate + + // type: KAboutPerson | { name?, task?, emailAddress?, webAddress?, avatarUrl? } + required property var modelData + + property bool hasAvatar: aboutItem.__hasAvatar(modelData) + + Layout.fillWidth: true + + spacing: Kirigami.Units.smallSpacing * 2 + + Kirigami.Icon { + id: avatarIcon + + implicitWidth: Kirigami.Units.iconSizes.medium + implicitHeight: implicitWidth + + fallback: "user" + source: { + if (delegate.hasAvatar && aboutItem.loadAvatars) { + // Appending to the params of the url does not work, thus the search is set + const url = new URL(modelData.avatarUrl); + const params = new URLSearchParams(url.search); + params.append("s", width); + url.search = params.toString(); + return url; + } else { + return "user" + } + } + visible: status !== Kirigami.Icon.Loading + } + + // So it's clear that something is happening while avatar images are loaded + QQC2.BusyIndicator { + implicitWidth: Kirigami.Units.iconSizes.medium + implicitHeight: implicitWidth + + visible: avatarIcon.status === Kirigami.Icon.Loading + running: visible + } + + QQC2.Label { + Layout.fillWidth: true + readonly property bool withTask: typeof(modelData.task) !== "undefined" && modelData.task.length > 0 + text: withTask ? qsTr("%1 (%2)").arg(modelData.name).arg(modelData.task) : modelData.name + wrapMode: Text.WordWrap + } + + QQC2.ToolButton { + visible: typeof(modelData.emailAddress) !== "undefined" && modelData.emailAddress.length > 0 + icon.name: "mail-sent" + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.text: qsTr("Send an email to %1").arg(modelData.emailAddress) + onClicked: Qt.openUrlExternally("mailto:%1".arg(modelData.emailAddress)) + } + + QQC2.ToolButton { + visible: typeof(modelData.webAddress) !== "undefined" && modelData.webAddress.length > 0 + icon.name: "globe" + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.text: (typeof(modelData.webAddress) === "undefined" && modelData.webAddress.length > 0) ? "" : modelData.webAddress + onClicked: Qt.openUrlExternally(modelData.webAddress) + } + } + } + + Kirigami.FormLayout { + id: form + + anchors.fill: parent + + GridLayout { + columns: 2 + Layout.fillWidth: true + + Kirigami.Icon { + Layout.rowSpan: 3 + Layout.preferredHeight: Kirigami.Units.iconSizes.huge + Layout.preferredWidth: height + Layout.maximumWidth: aboutItem.width / 3; + Layout.rightMargin: Kirigami.Units.largeSpacing + source: Kirigami.Settings.applicationWindowIcon || aboutItem.aboutData.programLogo || aboutItem.aboutData.programIconName || aboutItem.aboutData.componentName + } + + Kirigami.Heading { + Layout.fillWidth: true + text: aboutItem.aboutData.displayName + " " + aboutItem.aboutData.version + wrapMode: Text.WordWrap + } + + Kirigami.Heading { + Layout.fillWidth: true + level: 2 + wrapMode: Text.WordWrap + text: aboutItem.aboutData.shortDescription + } + + RowLayout { + spacing: Kirigami.Units.largeSpacing * 2 + + UrlButton { + text: qsTr("Get Involved") + url: aboutItem.getInvolvedUrl + visible: url.toString().length > 0 + } + + UrlButton { + text: qsTr("Donate") + url: aboutItem.donateUrl + visible: url.toString().length > 0 + } + + UrlButton { + readonly property string theUrl: { + if (aboutItem.aboutData.bugAddress !== "submit@bugs.kde.org") { + return aboutItem.aboutData.bugAddress + } + const elements = aboutItem.aboutData.productName.split('/'); + let url = `https://bugs.kde.org/enter_bug.cgi?format=guided&product=${elements[0]}&version=${aboutItem.aboutData.version}`; + if (elements.length === 2) { + url += "&component=" + elements[1]; + } + return url; + } + text: qsTr("Report a Bug") + url: theUrl + visible: theUrl.toString().length > 0 + } + } + } + + Separator { + Layout.fillWidth: true + } + + Kirigami.Heading { + Kirigami.FormData.isSection: true + text: qsTr("Copyright") + } + + QQC2.Label { + Layout.leftMargin: Kirigami.Units.gridUnit + text: aboutItem.aboutData.otherText + visible: text.length > 0 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + QQC2.Label { + Layout.leftMargin: Kirigami.Units.gridUnit + text: aboutItem.aboutData.copyrightStatement + visible: text.length > 0 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + UrlButton { + Layout.leftMargin: Kirigami.Units.gridUnit + url: aboutItem.aboutData.homepage + visible: url.length > 0 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + OverlaySheet { + id: licenseSheet + property alias text: bodyLabel.text + + contentItem: SelectableLabel { + id: bodyLabel + text: licenseSheet.text + wrapMode: Text.Wrap + } + } + + Component { + id: licenseLinkButton + + RowLayout { + Layout.leftMargin: Kirigami.Units.smallSpacing + + QQC2.Label { text: qsTr("License:") } + + LinkButton { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: modelData.name + onClicked: mouse => { + licenseSheet.text = modelData.text + licenseSheet.title = modelData.name + licenseSheet.open() + } + } + } + } + + Component { + id: licenseTextItem + + QQC2.Label { + Layout.leftMargin: Kirigami.Units.smallSpacing + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: qsTr("License: %1").arg(modelData.name) + } + } + + Repeater { + model: aboutItem.aboutData.licenses + delegate: _usePageStack ? licenseLinkButton : licenseTextItem + } + + Kirigami.Heading { + Kirigami.FormData.isSection: visible + text: qsTr("Libraries in use") + Layout.fillWidth: true + wrapMode: Text.WordWrap + visible: Kirigami.Settings.information + } + + Repeater { + model: Kirigami.Settings.information + delegate: QQC2.Label { + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.fillWidth: true + wrapMode: Text.WordWrap + id: libraries + text: modelData + } + } + + Repeater { + model: aboutItem.aboutData.components + delegate: QQC2.Label { + Layout.fillWidth: true + wrapMode: Text.WordWrap + Layout.leftMargin: Kirigami.Units.gridUnit + text: modelData.name + (modelData.version.length === 0 ? "" : " %1".arg(modelData.version)) + } + } + + Kirigami.Heading { + Layout.fillWidth: true + Kirigami.FormData.isSection: visible + text: qsTr("Authors") + wrapMode: Text.WordWrap + visible: aboutItem.aboutData.authors.length > 0 + } + + QQC2.CheckBox { + id: remoteAvatars + visible: aboutItem.__hasAvatars + checked: aboutItem.loadAvatars + onToggled: aboutItem.loadAvatars = checked + text: qsTr("Show author photos") + } + + Repeater { + id: authorsRepeater + model: aboutItem.aboutData.authors + delegate: personDelegate + } + + Kirigami.Heading { + Kirigami.FormData.isSection: visible + text: qsTr("Credits") + visible: repCredits.count > 0 + } + + Repeater { + id: repCredits + model: aboutItem.aboutData.credits + delegate: personDelegate + } + + Kirigami.Heading { + Kirigami.FormData.isSection: visible + text: qsTr("Translators") + visible: repTranslators.count > 0 + } + + Repeater { + id: repTranslators + model: aboutItem.aboutData.translators + delegate: personDelegate + } + } +} diff --git a/src/controls/AboutPage.qml b/src/controls/AboutPage.qml new file mode 100644 index 0000000..3d05d50 --- /dev/null +++ b/src/controls/AboutPage.qml @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +//TODO KF6: move somewhere else? kirigami addons? +/** + * @brief An "About" page that is ready to integrate in a Kirigami app. + * + * Allows to have a page that will show the copyright notice of the application + * together with the contributors and some information of which platform it's + * running on. + * + * @since 5.52 + * @since org.kde.kirigami 2.6 + * @inherit org::kde::kirigami::ScrollablePage + */ +Kirigami.ScrollablePage { + id: page + +//BEGIN properties + /** + * @brief This property holds an object with the same shape as KAboutData. + * + * For example: + * @code{json} + * aboutData: { + "displayName" : "KirigamiApp", + "productName" : "kirigami/app", + "componentName" : "kirigamiapp", + "shortDescription" : "A Kirigami example", + "homepage" : "", + "bugAddress" : "submit@bugs.kde.org", + "version" : "5.14.80", + "otherText" : "", + "authors" : [ + { + "name" : "...", + "task" : "", + "emailAddress" : "somebody@kde.org", + "webAddress" : "", + "ocsUsername" : "" + } + ], + "credits" : [], + "translators" : [], + "licenses" : [ + { + "name" : "GPL v2", + "text" : "long, boring, license text", + "spdx" : "GPL-2.0" + } + ], + "copyrightStatement" : "© 2010-2018 Plasma Development Team", + "desktopFileName" : "org.kde.kirigamiapp" + } + @endcode + * + * @see KAboutData + * @see org::kde::kirigami::AboutItem::aboutData + * @property KAboutData aboutData + */ + property alias aboutData: aboutItem.aboutData + + /** + * @brief This property holds a link to a "Get Involved" page. + * + * default: `"https://community.kde.org/Get_Involved" when your application id starts with "org.kde.", otherwise is empty` + * + * @property url getInvolvedUrl + */ + property alias getInvolvedUrl: aboutItem.getInvolvedUrl + + /** + * @brief This property holds a link to a "Donate" page. + * @since 5.101 + * + * default: `"https://kde.org/community/donations" when application id starts with "org.kde.", otherwise it is empty.` + */ + property alias donateUrl: aboutItem.donateUrl + + /** + * @brief This property controls whether to load avatars by URL. + * + * If set to false, a fallback "user" icon will be displayed. + * + * default: ``false`` + */ + property alias loadAvatars: aboutItem.loadAvatars + + /** @internal */ + default property alias _content: aboutItem._content +//END properties + + title: qsTr("About %1").arg(page.aboutData.displayName) + + Kirigami.AboutItem { + id: aboutItem + wideMode: page.width >= aboutItem.implicitWidth + + _usePageStack: applicationWindow().pageStack ? true : false + } +} diff --git a/src/controls/AbstractApplicationHeader.qml b/src/controls/AbstractApplicationHeader.qml new file mode 100644 index 0000000..4172fb0 --- /dev/null +++ b/src/controls/AbstractApplicationHeader.qml @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami +import "private" as P +import "templates" as T + + +//TODO KF6: remove +/** + * @brief An item that can be used as a title for the application. + * + * Scrolling the main page will make it taller or shorter (through the point of going away) + * It's a behavior similar to the typical mobile web browser addressbar + * the minimum, preferred and maximum heights of the item can be controlled with + * * ``minimumHeight``: default is 0, i.e. hidden + * * ``preferredHeight``: default is Kirigami.Units.gridUnit * 1.6 + * * ``maximumHeight``: default is Kirigami.Units.gridUnit * 3 + * + * To achieve a titlebar that stays completely fixed just set the 3 sizes as the same + * + * @inherit org::kde::kirigami::templates::AbstractApplicationHeader + */ +T.AbstractApplicationHeader { + id: root + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Header + + background: Rectangle { + color: Kirigami.Theme.backgroundColor + P.EdgeShadow { + id: shadow + visible: root.separatorVisible + anchors { + right: parent.right + left: parent.left + top: parent.bottom + } + edge: Qt.TopEdge + opacity: (!root.page || !root.page.header || root.page.header.toString().indexOf("ToolBar") === -1) + Behavior on opacity { + OpacityAnimator { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + Behavior on opacity { + OpacityAnimator { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + } +} diff --git a/src/controls/AbstractApplicationItem.qml b/src/controls/AbstractApplicationItem.qml new file mode 100644 index 0000000..e0673cf --- /dev/null +++ b/src/controls/AbstractApplicationItem.qml @@ -0,0 +1,355 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Templates as T + +import org.kde.kirigami as Kirigami +import "templates/private" as TP +import "templates" as KT + +/** + * @brief An item that provides the features of AbstractApplicationWindow without the window itself. + * + * This allows embedding into a larger application. + * Unless you need extra flexibility it is recommended to use ApplicationItem instead. + * + * Example usage: + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.AbstractApplicationItem { + * [...] + * globalDrawer: Kirigami.GlobalDrawer { + * actions: [ + * Kirigami.Action { + * text: "View" + * icon.name: "view-list-icons" + * Kirigami.Action { + * text: "action 1" + * } + * Kirigami.Action { + * text: "action 2" + * } + * Kirigami.Action { + * text: "action 3" + * } + * }, + * Kirigami.Action { + * text: "Sync" + * icon.name: "folder-sync" + * } + * ] + * } + * + * contextDrawer: Kirigami.ContextDrawer { + * id: contextDrawer + * } + * + * pageStack: Kirigami.PageRow { + * ... + * } + * [...] + * } + * @endcode + * + * @inherit QtQuick.Item + */ +Item { + id: root + +//BEGIN properties + /** + * @brief This property holds the stack used to allocate the pages and to manage the + * transitions between them. + * + * Put a container here, such as QtQuick.Controls.StackView. + */ + property Item pageStack + + /** + * @brief This property holds the font for this item. + * + * default: ``Kirigami.Theme.defaultFont`` + */ + property font font: Kirigami.Theme.defaultFont + + /** + * @brief This property holds the locale for this item. + */ + property Locale locale + + /** + * @brief This property holds an item that can be used as a menuBar for the application. + */ + property T.MenuBar menuBar + + /** + * @brief This property holds an item that can be used as a title for the application. + * + * Scrolling the main page will make it taller or shorter (through the point of going away). + * + * It's a behavior similar to the typical mobile web browser addressbar. + * + * The minimum, preferred and maximum heights of the item can be controlled with + * + * * ``Layout.minimumHeight``: default is 0, i.e. hidden + * * ``Layout.preferredHeight``: default is Kirigami.Units.gridUnit * 1.6 + * * ``Layout.maximumHeight``: default is Kirigami.Units.gridUnit * 3 + * + * To achieve a titlebar that stays completely fixed, just set the 3 sizes as the same. + * + * @property kirigami::templates::AbstractApplicationHeader header + */ + property KT.AbstractApplicationHeader header + + /** + * @brief This property holds an item that can be used as a footer for the application. + */ + property Item footer + + /** + * @brief This property sets whether the standard chrome of the app is visible. + * + * These are the action button, the drawer handles and the application header. + * + * default: ``true`` + */ + property bool controlsVisible: true + + /** + * @brief This property holds the drawer for global actions. + * + * Thos drawer can be opened by sliding from the left screen edge + * or by dragging the ActionButton to the right. + * + * @note It is recommended to use the GlobalDrawer here. + * @property org::kde::kirigami::OverlayDrawer globalDrawer + */ + property OverlayDrawer globalDrawer + + /** + * @brief This property tells us whether the application is in "widescreen" mode. + * + * This is enabled on desktops or horizontal tablets. + * + * @note Different styles can have their own logic for deciding this. + */ + property bool wideScreen: width >= Kirigami.Units.gridUnit * 60 + + /** + * @brief This property holds the drawer for context-dependent actions. + * + * The drawer that will be opened by sliding from the right screen edge + * or by dragging the ActionButton to the left. + * + * @note It is recommended to use the ContextDrawer type here. + * + * The contents of the context drawer should depend from what page is + * loaded in the main pageStack + * + * Example usage: + * + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.ApplicationWindow { + * contextDrawer: Kirigami.ContextDrawer { + * enabled: true + * actions: [ + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * }, + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * } + * ] + * } + * } + * @endcode + * + * @property org::kde::kirigami::ContextDrawer contextDrawer + */ + property OverlayDrawer contextDrawer + + /** + * @brief This property holds the list of all children of this item. + * @internal + * @property list __data + */ + default property alias __data: contentItemRoot.data + + /** + * @brief This property holds the Item of the main part of the Application UI. + */ + readonly property Item contentItem: Item { + id: contentItemRoot + parent: root + anchors { + fill: parent + topMargin: controlsVisible ? (root.header ? root.header.height : 0) + (root.menuBar ? root.menuBar.height : 0) : 0 + bottomMargin: controlsVisible && root.footer ? root.footer.height : 0 + leftMargin: root.globalDrawer && root.globalDrawer.modal === false ? root.globalDrawer.contentItem.width * root.globalDrawer.position : 0 + rightMargin: root.contextDrawer && root.contextDrawer.modal === false ? root.contextDrawer.contentItem.width * root.contextDrawer.position : 0 + } + } + + /** + * @brief This property holds the color for the background. + * + * default: ``Kirigami.Theme.backgroundColor`` + */ + property color color: Kirigami.Theme.backgroundColor + + /** + * @brief This property holds the background of the Application UI. + */ + property Item background + + property alias overlay: overlayRoot +//END properties + +//BEGIN functions + /** + * @brief This function shows a little passive notification at the bottom of the app window + * lasting for few seconds, with an optional action button. + * + * @param message The text message to be shown to the user. + * @param timeout How long to show the message: + * possible values: "short", "long" or the number of milliseconds + * @param actionText Text in the action button, if any. + * @param callBack A JavaScript function that will be executed when the + * user clicks the button. + */ + function showPassiveNotification(message, timeout, actionText, callBack) { + notificationsObject.showNotification(message, timeout, actionText, callBack); + } + + /** + * @brief This function hides the passive notification at specified index, if any is shown. + * @param index Index of the notification to hide. Default is 0 (oldest notification). + */ + function hidePassiveNotification(index = 0) { + notificationsObject.hideNotification(index); + } + + /** + * @brief This property gets application windows object anywhere in the application. + * @returns a pointer to this item. + */ + function applicationWindow() { + return root; + } +//END functions + +//BEGIN signals handlers + onMenuBarChanged: { + if (menuBar) { + menuBar.parent = root.contentItem + if (menuBar instanceof T.ToolBar) { + menuBar.position = T.ToolBar.Footer + } else if (menuBar instanceof T.DialogButtonBox) { + menuBar.position = T.DialogButtonBox.Footer + } + menuBar.width = Qt.binding(() => root.contentItem.width) + // FIXME: (root.header.height ?? 0) when we can depend from 5.15 + menuBar.y = Qt.binding(() => -menuBar.height - (root.header.height ? root.header.height : 0)) + } + } + + onHeaderChanged: { + if (header) { + header.parent = root.contentItem + if (header instanceof T.ToolBar) { + header.position = T.ToolBar.Header + } else if (header instanceof T.DialogButtonBox) { + header.position = T.DialogButtonBox.Header + } + header.width = Qt.binding(() => root.contentItem.width) + header.y = Qt.binding(() => -header.height) + } + } + + onFooterChanged: { + if (footer) { + footer.parent = root.contentItem + if (footer instanceof T.ToolBar) { + footer.position = T.ToolBar.Footer + } else if (footer instanceof T.DialogButtonBox) { + footer.position = T.DialogButtonBox.Footer + } + footer.width = Qt.binding(() => root.contentItem.width) + footer.y = Qt.binding(() => root.contentItem.height) + } + } + + onBackgroundChanged: { + if (background) { + background.parent = root.contentItem + background.anchors.fill = background.parent + } + } + + onPageStackChanged: pageStack.parent = root.contentItem; +//END signals handlers + + LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft + LayoutMirroring.childrenInherit: true + + TP.PassiveNotificationsManager { + id: notificationsObject + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + z: 1 + } + + Item { + anchors.fill: parent + parent: root.parent || root + z: 999999 + + Item { + id: overlayRoot + z: -1 + anchors.fill: parent + } + } + + // Don't use root.overlay property here. For one, we know that in a window + // it will always be the same as T.Overlay.overlay; secondly Drawers + // don't care about being contained/parented to anything else anyway. + onGlobalDrawerChanged: { + if (globalDrawer) { + globalDrawer.parent = Qt.binding(() => visible ? T.Overlay.overlay : null); + } + } + onContextDrawerChanged: { + if (contextDrawer) { + contextDrawer.parent = Qt.binding(() => visible ? T.Overlay.overlay : null); + } + } + + Window.onWindowChanged: { + if (globalDrawer) { + globalDrawer.visible = globalDrawer.drawerOpen; + } + if (contextDrawer) { + contextDrawer.visible = contextDrawer.drawerOpen; + } + } + + implicitWidth: Kirigami.Settings.isMobile ? Kirigami.Units.gridUnit * 30 : Kirigami.Units.gridUnit * 55 + implicitHeight: Kirigami.Settings.isMobile ? Kirigami.Units.gridUnit * 45 : Kirigami.Units.gridUnit * 40 + visible: true +} diff --git a/src/controls/AbstractApplicationWindow.qml b/src/controls/AbstractApplicationWindow.qml new file mode 100644 index 0000000..319523c --- /dev/null +++ b/src/controls/AbstractApplicationWindow.qml @@ -0,0 +1,289 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami +import "templates/private" as TP + +/** + * A window that provides some basic features needed for all apps + * Use this class only if you need a custom content for your application, + * different from the Page Row behavior recommended by the HIG and provided + * by ApplicationWindow. + * It is recommended to use ApplicationWindow instead + * @see ApplicationWindow + * + * It's usually used as a root QML component for the application. + * It provides support for a central page stack, side drawers, and + * basic support for the Android back button. + * + * Setting a width and height property on the ApplicationWindow + * will set its initial size, but it won't set it as an automatically binding. + * to resize programmatically the ApplicationWindow they need to + * be assigned again in an imperative fashion + * + * + * Example usage: + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.ApplicationWindow { + * [...] + * globalDrawer: Kirigami.GlobalDrawer { + * actions: [ + * Kirigami.Action { + * text: "View" + * icon.name: "view-list-icons" + * Kirigami.Action { + * text: "action 1" + * } + * Kirigami.Action { + * text: "action 2" + * } + * Kirigami.Action { + * text: "action 3" + * } + * }, + * Kirigami.Action { + * text: "Sync" + * icon.name: "folder-sync" + * } + * ] + * } + * + * contextDrawer: Kirigami.ContextDrawer { + * id: contextDrawer + * } + * + * pageStack: PageStack { + * ... + * } + * [...] + * } + * @endcode + * + * @inherit QtQuick.Controls.ApplicationWindow + */ +QQC2.ApplicationWindow { + id: root + +//BEGIN properties + /** + * @brief This property holds the stack used to allocate the pages and to manage the + * transitions between them. + * + * Put a container here, such as QtQuick.Controls.StackView. + */ + property Item pageStack + + /** + * @brief This property sets whether the standard chrome of the app is visible. + * + * These are the action button, the drawer handles, and the application header. + * + * default: ``true`` + */ + property bool controlsVisible: true + + /** + * @brief This property holds the drawer for global actions. + * + * This drawer can be opened by sliding from the left screen edge + * or by dragging the ActionButton to the right. + * + * @note It is recommended to use the GlobalDrawer here. + * @property org::kde::kirigami::OverlayDrawer globalDrawer + */ + property OverlayDrawer globalDrawer + + /** + * @brief This property tells whether the application is in "widescreen" mode. + * + * This is enabled on desktops or horizontal tablets. + * + * @note Different styles can have their own logic for deciding this. + */ + property bool wideScreen: width >= Kirigami.Units.gridUnit * 60 + + /** + * @brief This property holds the drawer for context-dependent actions. + * + * The drawer that will be opened by sliding from the right screen edge + * or by dragging the ActionButton to the left. + * + * @note It is recommended to use the ContextDrawer type here. + * + * The contents of the context drawer should depend from what page is + * loaded in the main pageStack + * + * Example usage: + * + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.ApplicationWindow { + * contextDrawer: Kirigami.ContextDrawer { + * enabled: true + * actions: [ + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * }, + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * } + * ] + * } + * } + * @endcode + * + * @property org::kde::kirigami::ContextDrawer contextDrawer + */ + property OverlayDrawer contextDrawer + + /** + * Effectively the same as T.Overlay.overlay + */ + readonly property Item overlay: T.Overlay.overlay + + /** + * This property holds a standard action that will quit the application when triggered. + * Its properties have the following values: + * + * @code + * Action { + * text: "Quit" + * icon.name: "application-exit-symbolic" + * shortcut: StandardKey.Quit + * // ... + * } + * @endcode + * @since 5.76 + */ + readonly property Kirigami.Action quitAction: Kirigami.Action { + text: qsTr("Quit") + icon.name: "application-exit"; + shortcut: StandardKey.Quit + onTriggered: source => root.close(); + } +//END properties + +//BEGIN functions + /** + * @brief This function shows a little passive notification at the bottom of the app window + * lasting for few seconds, with an optional action button. + * + * @param message The text message to be shown to the user. + * @param timeout How long to show the message: + * possible values: "short", "long" or the number of milliseconds + * @param actionText Text in the action button, if any. + * @param callBack A JavaScript function that will be executed when the + * user clicks the button. + */ + function showPassiveNotification(message, timeout, actionText, callBack) { + notificationsObject.showNotification(message, timeout, actionText, callBack); + } + + /** + * @brief This function hides the passive notification at specified index, if any is shown. + * @param index Index of the notification to hide. Default is 0 (oldest notification). + */ + function hidePassiveNotification(index = 0) { + notificationsObject.hideNotification(index); + } + + /** + * @brief This function returns application window's object anywhere in the application. + * @returns a pointer to this application window + * can be used anywhere in the application. + */ + function applicationWindow() { + return root; + } +//END functions + + LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft + LayoutMirroring.childrenInherit: true + + color: Kirigami.Theme.backgroundColor + + TP.PassiveNotificationsManager { + id: notificationsObject + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + z: 1 + } + + contentItem.z: 1 + contentItem.anchors.left: contentItem.parent.left + contentItem.anchors.right: contentItem.parent.right + contentItem.anchors.topMargin: root.wideScreen && header && controlsVisible ? header.height : 0 + contentItem.anchors.leftMargin: root.globalDrawer && root.globalDrawer.modal === false && (!root.pageStack || root.pageStack.leftSidebar !== root.globalDrawer) ? root.globalDrawer.width * root.globalDrawer.position : 0 + contentItem.anchors.rightMargin: root.contextDrawer && root.contextDrawer.modal === false ? root.contextDrawer.width * root.contextDrawer.position : 0 + + Binding { + target: root.header + property: "x" + value: -contentItem.x + } + Binding { + target: root.footer + property: "x" + value: -contentItem.x + } + + // Don't use root.overlay property here. For one, we know that in a window + // it will always be the same as T.Overlay.overlay; secondly Drawers + // don't care about being contained/parented to anything else anyway. + onGlobalDrawerChanged: { + if (globalDrawer) { + globalDrawer.parent = Qt.binding(() => T.Overlay.overlay); + } + } + onContextDrawerChanged: { + if (contextDrawer) { + contextDrawer.parent = Qt.binding(() => T.Overlay.overlay); + } + } + onPageStackChanged: { + if (pageStack) { + // contentItem is declared as CONSTANT, so binding isn't needed. + pageStack.parent = contentItem; + } + } + + width: Kirigami.Settings.isMobile ? Kirigami.Units.gridUnit * 30 : Kirigami.Units.gridUnit * 55 + height: Kirigami.Settings.isMobile ? Kirigami.Units.gridUnit * 45 : Kirigami.Units.gridUnit * 40 + visible: true + + Component.onCompleted: { + // Explicitly break the binding as we need this to be set only at startup. + // if the bindings are active, after this the window is resized by the + // compositor and then the bindings are reevaluated, then the window + // size would reset ignoring what the compositor asked. + // see BUG 433849 + root.width = root.width; + root.height = root.height; + } + + // This is needed because discover in mobile mode does not + // close with the global drawer open. + Shortcut { + sequence: root.quitAction.shortcut + enabled: root.quitAction.enabled + context: Qt.ApplicationShortcut + onActivated: root.close(); + } +} diff --git a/src/controls/AbstractCard.qml b/src/controls/AbstractCard.qml new file mode 100644 index 0000000..8284cc2 --- /dev/null +++ b/src/controls/AbstractCard.qml @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import "templates" as T +import "private" as P + +/** + * @brief AbstractCard is the base for cards. + * + * A Card is a visual object that serves as an entry point for more detailed information. + * An abstractCard is empty, providing just the look and the base properties and signals + * for an ItemDelegate. It can be filled with any custom layout of items, + * its content is organized in 3 properties: header, contentItem and footer. + * Use this only when you need particular custom contents, for a standard layout + * for cards, use the Card component. + * + * @see org::kde::kirigami::Card + * @inherit org::kde::kirigami::templates::AbstractCard + * @since 2.4 + */ +T.AbstractCard { + id: root + + background: P.DefaultCardBackground { + clickFeedback: root.showClickFeedback + hoverFeedback: root.hoverEnabled + } +} diff --git a/src/controls/Action.qml b/src/controls/Action.qml new file mode 100644 index 0000000..655a1c2 --- /dev/null +++ b/src/controls/Action.qml @@ -0,0 +1,160 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQml +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami +import org.kde.kirigami.private as P + +/** + * @brief An item that represents an abstract Action + * @inherit QtQuick.QQC2.Action + */ +QQC2.Action { +//BEGIN properties + /** + * @brief This property holds whether the graphic representation of the action + * is supposed to be visible. + * + * It's up to the action representation to honor this property. + * + * default: ``true`` + */ + property bool visible: !fromQAction || fromQAction.visible + + /** + * @brief This property holds the tooltip text that is shown when the cursor is hovering over the control. + * + * Leaving this undefined or setting it to an empty string means that no tooltip will be shown when + * the cursor is hovering over the control that triggers the tooltip. + * @warning Tooltips may not be supported on all platforms. + */ + property string tooltip + + /** + * @brief This property sets whether this action is a separator action. + * + * default: ``false`` + */ + property bool separator: false + + /** + * @brief This property holds whether auto-exclusivity is enabled. + * + * If auto-exclusivity is enabled, checkable actions that belong to the + * same parent item behave as if they were part of the same ButtonGroup. + * Only one action can be checked at any time; checking another action + * automatically unchecks the previously checked one. + * + * default: ``false`` + */ + property bool autoExclusive: false + + /** + * @brief This property sets whether this action becomes a title displaying + * its child actions as sub-items in GlobalDrawers and ContextDrawers. + * + * default: ``false`` + * + * @since 2.6 + */ + property bool expandible: false + + /** + * @brief This property holds the parent action. + */ + property T.Action parent + + /** + * @brief This property sets this action's display type. + * + * These are provided to implementations to indicate a preference for certain display + * styles. + * + * default: ``Kirigami.DisplayHint.NoPreference`` + * + * @note This property contains only preferences, implementations may choose to disregard them. + * @see org::kde::kirigami::DisplayHint + * @since 2.12 + */ + property int displayHint: Kirigami.DisplayHint.NoPreference + + /** + * @brief This property holds the component that should be used for displaying this action. + * @note This can be used to display custom components in the toolbar. + * @since 5.65 + * @since 2.12 + */ + property Component displayComponent + + /** + * @brief This property holds a list of child actions. + * + * This is useful for tree-like menus, such as the GlobalDrawer. + * + * Example usage: + * @code + * import QtQuick.Controls as QQC2 + * import org.kde.kirigami as Kirigami + * + * Kirigami.Action { + * text: "Tools" + * + * QQC2.Action { + * text: "Action1" + * } + * Kirigami.Action { + * text: "Action2" + * } + * } + * @endcode + * @property list children + */ + default property list children + + /** + * This property holds a QAction + * + * When provided Kirigami.Action will be initialized from the given QAction. + * + * @since Kirigami 6.4.0 + */ + property QtObject fromQAction +//END properties + + onChildrenChanged: { + children + .filter(action => action instanceof Kirigami.Action) + .forEach(action => { + action.parent = this; + }); + } + + /** + * @brief This property holds the action's visible child actions. + * @property list visibleChildren + */ + readonly property list visibleChildren: children + .filter(action => !(action instanceof Kirigami.Action) || action.visible) + + shortcut: fromQAction?.shortcut + text: fromQAction?.text ?? '' + icon.name: fromQAction ? P.ActionHelper.iconName(fromQAction.icon) : '' + onTriggered: if (fromQAction) { + fromQAction.trigger(); + } + checkable: fromQAction?.checkable ?? false + checked: fromQAction?.checked ?? false + enabled: !fromQAction || fromQAction.enabled + + readonly property Shortcut alternateShortcut : Shortcut { + sequences: P.ActionHelper.alternateShortcuts(fromQAction) + onActivated: root.trigger() + } +} diff --git a/src/controls/ActionTextField.qml b/src/controls/ActionTextField.qml new file mode 100644 index 0000000..a5bb2b8 --- /dev/null +++ b/src/controls/ActionTextField.qml @@ -0,0 +1,191 @@ +/* + * SPDX-FileCopyrightText: 2019 Carl-Lucien Schwan + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami + +/** + * This is advanced textfield. It is recommended to use this class when there + * is a need to create a create a textfield with action buttons (e.g a clear + * action). + * + * Example usage for a search field: + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.ActionTextField { + * id: searchField + * + * placeholderText: i18n("Search…") + * + * focusSequence: StandardKey.Find + * + * rightActions: Kirigami.Action { + * icon.name: "edit-clear" + * visible: searchField.text.length > 0 + * onTriggered: { + * searchField.clear(); + * searchField.accepted(); + * } + * } + * + * onAccepted: console.log("Search text is " + searchField.text); + * } + * @endcode + * + * @since 5.56 + * @inherit QtQuick.Controls.TextField + */ +QQC2.TextField { + id: root + + /** + * @brief This property holds a shortcut sequence that will focus the text field. + * @since 5.56 + */ + property alias focusSequence: focusShortcut.sequence + + /** + * @brief This property holds a list of actions that will be displayed on the left side of the text field. + * + * By default this list is empty. + * + * @since 5.56 + */ + property list leftActions + + /** + * @brief This property holds a list of actions that will be displayed on the right side of the text field. + * + * By default this list is empty. + * + * @since 5.56 + */ + property list rightActions + + property alias _leftActionsRow: leftActionsRow + property alias _rightActionsRow: rightActionsRow + + hoverEnabled: true + + // Manually setting this fixes alignment in RTL layouts + horizontalAlignment: TextInput.AlignLeft + + leftPadding: Kirigami.Units.smallSpacing + (root.effectiveHorizontalAlignment === TextInput.AlignRight ? rightActionsRow : leftActionsRow).width + rightPadding: Kirigami.Units.smallSpacing + (root.effectiveHorizontalAlignment === TextInput.AlignRight ? leftActionsRow : rightActionsRow).width + + Behavior on leftPadding { + NumberAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + + Behavior on rightPadding { + NumberAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + + Shortcut { + id: focusShortcut + enabled: root.visible && root.enabled + onActivated: { + root.forceActiveFocus(Qt.ShortcutFocusReason) + root.selectAll() + } + } + + QQC2.ToolTip { + visible: focusShortcut.nativeText.length > 0 && root.text.length === 0 && root.hovered + text: focusShortcut.nativeText + } + + component InlineActionIcon: Kirigami.Icon { + id: iconDelegate + + required property T.Action modelData + + implicitWidth: Kirigami.Units.iconSizes.sizeForLabels + implicitHeight: Kirigami.Units.iconSizes.sizeForLabels + + anchors.verticalCenter: parent?.verticalCenter + + source: modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source + visible: !(modelData instanceof Kirigami.Action) || modelData.visible + active: actionArea.containsPress || actionArea.activeFocus + enabled: modelData.enabled + + MouseArea { + id: actionArea + + anchors.fill: parent + activeFocusOnTab: true + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + + Accessible.name: iconDelegate.modelData.text + Accessible.role: Accessible.Button + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Space: + case Qt.Key_Enter: + case Qt.Key_Return: + case Qt.Key_Select: + clicked(null); + event.accepted = true; + break; + } + } + onClicked: mouse => iconDelegate.modelData.trigger() + } + + QQC2.ToolTip { + visible: (actionArea.containsMouse || actionArea.activeFocus) && (iconDelegate.modelData.text.length > 0) + text: iconDelegate.modelData.text + } + } + + Row { + id: leftActionsRow + padding: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing + layoutDirection: Qt.LeftToRight + LayoutMirroring.enabled: root.effectiveHorizontalAlignment === TextInput.AlignRight + anchors.left: parent.left + anchors.leftMargin: Kirigami.Units.smallSpacing + anchors.top: parent.top + anchors.topMargin: parent.topPadding + anchors.bottom: parent.bottom + anchors.bottomMargin: parent.bottomPadding + Repeater { + model: root.leftActions + InlineActionIcon { } + } + } + + Row { + id: rightActionsRow + padding: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing + layoutDirection: Qt.RightToLeft + LayoutMirroring.enabled: root.effectiveHorizontalAlignment === TextInput.AlignRight + anchors.right: parent.right + anchors.rightMargin: Kirigami.Units.smallSpacing + anchors.top: parent.top + anchors.topMargin: parent.topPadding + anchors.bottom: parent.bottom + anchors.bottomMargin: parent.bottomPadding + Repeater { + model: root.rightActions + InlineActionIcon { } + } + } +} diff --git a/src/controls/ActionToolBar.qml b/src/controls/ActionToolBar.qml new file mode 100644 index 0000000..5915094 --- /dev/null +++ b/src/controls/ActionToolBar.qml @@ -0,0 +1,244 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQml +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami +import "private" as P + +/** + * @brief A toolbar built out of a list of actions. + * + * The default representation for visible actions is a QtQuick.Controls.ToolButton, but + * it can be changed by setting the `Action.displayComponent` for an action. + * The default behavior of ActionToolBar is to display as many actions as possible, + * placing those that will not fit into an overflow menu. This can be changed by + * setting the `displayHint` property on an Action. For example, when setting the + * `DisplayHint.KeepVisible` display hint, ActionToolBar will try to keep that action + * in view as long as possible, using an icon-only button if a button with text + * does not fit. + * + * @inherit QtQuick.Controls.Control + * @since 2.5 + */ +QQC2.Control { + id: root + +//BEGIN properties + /** + * @brief This property holds a list of visible actions. + * + * The ActionToolBar will try to display as many actions as possible. + * Those that won't fit will go into an overflow menu. + * + * @property list actions + */ + readonly property alias actions: layout.actions + + /** + * @brief This property holds whether the buttons will have a flat appearance. + * + * default: ``true`` + */ + property bool flat: true + + /** + * @brief This property determines how the icon and text are displayed within the button. + * + * Permitted values are: + * * ``Button.IconOnly`` + * * ``Button.TextOnly`` + * * ``Button.TextBesideIcon`` + * * ``Button.TextUnderIcon`` + * + * default: ``Controls.Button.TextBesideIcon`` + * + * @see QtQuick.Controls.AbstractButton + * @property int display + */ + property int display: QQC2.Button.TextBesideIcon + + /** + * @brief This property holds the alignment of the buttons. + * + * When there is more space available than required by the visible delegates, + * we need to determine how to place the delegates. + * + * When there is more space available than required by the visible action delegates, + * we need to determine where to position them. + * + * default: ``Qt.AlignLeft`` + * + * @see Qt::AlignmentFlag + * @property int alignment + */ + property alias alignment: layout.alignment + + /** + * @brief This property holds the position of the toolbar. + * + * If this ActionToolBar is the contentItem of a QQC2 Toolbar, the position is bound to the ToolBar's position + * + * Permitted values are: + * * ``ToolBar.Header``: The toolbar is at the top, as a window or page header. + * * ``ToolBar.Footer``: The toolbar is at the bottom, as a window or page footer. + * + * @property int position + */ + property int position: parent instanceof T.ToolBar ? parent.position : QQC2.ToolBar.Header + + /** + * @brief This property holds the maximum width of the content of this ToolBar. + * + * If the toolbar's width is larger than this value, empty space will + * be added on the sides, according to the Alignment property. + * + * The value of this property is derived from the ToolBar's actions and their properties. + * + * @property int maximumContentWidth + */ + readonly property alias maximumContentWidth: layout.implicitWidth + + /** + * @brief This property holds the name of the icon to use for the overflow menu button. + * + * default: ``"overflow-menu"`` + * + * @since 5.65 + * @since 2.12 + */ + property string overflowIconName: "overflow-menu" + + /** + * @brief This property holds the combined width of all visible delegates. + * @property int visibleWidth + */ + readonly property alias visibleWidth: layout.visibleWidth + + /** + * @brief This property sets the handling method for items that do not match the toolbar's height. + * + * When toolbar items do not match the height of the toolbar, there are + * several ways we can deal with this. This property sets the preferred way. + * + * Permitted values are: + * * ``HeightMode.AlwaysCenter`` + * * ``HeightMode.AlwaysFill`` + * * ``AlwaysFill.ConstrainIfLarger`` + * + * default: ``HeightMode::ConstrainIfLarger`` + * + * @see ToolBarLayout::heightMode + * @see ToolBarLayout::HeightMode + * @property ToolBarLayout::HeightMode heightMode + */ + property alias heightMode: layout.heightMode +//END properties + + implicitHeight: layout.implicitHeight + implicitWidth: layout.implicitWidth + + Layout.minimumWidth: layout.minimumWidth + Layout.preferredWidth: 0 + Layout.fillWidth: true + + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + Accessible.role: Accessible.ToolBar + + contentItem: Kirigami.ToolBarLayout { + id: layout + spacing: Kirigami.Units.smallSpacing + layoutDirection: root.mirrored ? Qt.RightToLeft : Qt.LeftToRight + + fullDelegate: P.PrivateActionToolButton { + flat: root.flat + display: root.display + action: Kirigami.ToolBarLayout.action + } + + iconDelegate: P.PrivateActionToolButton { + flat: root.flat + display: QQC2.Button.IconOnly + action: Kirigami.ToolBarLayout.action + + showMenuArrow: false + + menuActions: { + if (action.displayComponent) { + return [action] + } + + if (action instanceof Kirigami.Action) { + return action.children; + } + + return [] + } + } + + separatorDelegate: QQC2.ToolSeparator {} + + moreButton: P.PrivateActionToolButton { + flat: root.flat + Accessible.role: Accessible.ButtonMenu + + action: Kirigami.Action { + tooltip: qsTr("More Actions") + icon.name: root.overflowIconName + displayHint: Kirigami.DisplayHint.IconOnly | Kirigami.DisplayHint.HideChildIndicator + } + + Accessible.name: action.tooltip + + menuActions: root.actions + + menuComponent: P.ActionsMenu { + submenuComponent: P.ActionsMenu { + Binding { + target: parentItem + property: "visible" + value: layout.hiddenActions.includes(parentAction) + && (!(parentAction instanceof Kirigami.Action) || parentAction.visible) + restoreMode: Binding.RestoreBinding + } + + Binding { + target: parentItem + property: "autoExclusive" + value: action instanceof Kirigami.Action && action.autoExclusive + restoreMode: Binding.RestoreBinding + } + } + + itemDelegate: P.ActionMenuItem { + visible: layout.hiddenActions.includes(action) + && (!(action instanceof Kirigami.Action) || action.visible) + autoExclusive: action instanceof Kirigami.Action && action.autoExclusive + } + + loaderDelegate: Loader { + property T.Action action + height: visible ? implicitHeight : 0 + visible: layout.hiddenActions.includes(action) + && (!(action instanceof Kirigami.Action) || action.visible) + } + + separatorDelegate: QQC2.MenuSeparator { + property T.Action action + visible: layout.hiddenActions.includes(action) + && (!(action instanceof Kirigami.Action) || action.visible) + } + } + } + } +} diff --git a/src/controls/ApplicationItem.qml b/src/controls/ApplicationItem.qml new file mode 100644 index 0000000..fbc0841 --- /dev/null +++ b/src/controls/ApplicationItem.qml @@ -0,0 +1,141 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +/** + * @brief An item that provides the features of ApplicationWindow without the window itself. + * + * This allows embedding into a larger application. + * It's based around the PageRow component that allows adding/removing of pages. + * + * Example usage: + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.ApplicationItem { + * globalDrawer: Kirigami.GlobalDrawer { + * actions: [ + * Kirigami.Action { + * text: "View" + * icon.name: "view-list-icons" + * Kirigami.Action { + * text: "action 1" + * } + * Kirigami.Action { + * text: "action 2" + * } + * Kirigami.Action { + * text: "action 3" + * } + * }, + * Kirigami.Action { + * text: "Sync" + * icon.name: "folder-sync" + * } + * ] + * } + * + * contextDrawer: Kirigami.ContextDrawer { + * id: contextDrawer + * } + * + * pageStack.initialPage: Kirigami.Page { + * mainAction: Kirigami.Action { + * icon.name: "edit" + * onTriggered: { + * // do stuff + * } + * } + * contextualActions: [ + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * }, + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * } + * ] + * // ... + * } + * } + * @endcode +*/ +Kirigami.AbstractApplicationItem { + id: root + + /** + * @brief This property holds the PageRow used to allocate the pages and + * manage the transitions between them. + * + * It's using a PageRow, while having the same API as PageStack, + * it positions the pages as adjacent columns, with as many columns + * as can fit in the screen. An handheld device would usually have a single + * fullscreen column, a tablet device would have many tiled columns. + * + * @property org::kde::kirigami::PageRow pageStack + */ + readonly property alias pageStack: __pageStack + + // Redefines here as here we can know a pointer to PageRow + wideScreen: width >= applicationWindow().pageStack.defaultColumnWidth * 2 + + Component.onCompleted: { + pageStack.currentItem?.forceActiveFocus(); + } + + Kirigami.PageRow { + id: __pageStack + anchors { + fill: parent + } + + function goBack() { + // NOTE: drawers are handling the back button by themselves + const backEvent = {accepted: false} + if (root.pageStack.currentIndex >= 1) { + root.pageStack.currentItem.backRequested(backEvent); + if (!backEvent.accepted) { + root.pageStack.flickBack(); + backEvent.accepted = true; + } + } + + if (Kirigami.Settings.isMobile && !backEvent.accepted && Qt.platform.os !== "ios") { + Qt.quit(); + } + } + function goForward() { + root.pageStack.currentIndex = Math.min(root.pageStack.depth - 1, root.pageStack.currentIndex + 1); + } + Keys.onBackPressed: event => { + goBack(); + event.accepted = true; + } + Shortcut { + sequences: [StandardKey.Forward] + onActivated: __pageStack.goForward(); + } + Shortcut { + sequences: [StandardKey.Back] + onActivated: __pageStack.goBack(); + } + + background: Rectangle { + color: root.color + } + + focus: true + } +} diff --git a/src/controls/ApplicationWindow.qml b/src/controls/ApplicationWindow.qml new file mode 100644 index 0000000..b988034 --- /dev/null +++ b/src/controls/ApplicationWindow.qml @@ -0,0 +1,120 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +/** + * @brief A window that provides some basic features needed for all apps + * + * It's usually used as a root QML component for the application. + * It's based around the PageRow component, the application will be + * about pages adding and removal. + * For most of the usages, this class should be used instead + * of AbstractApplicationWindow + * @see AbstractApplicationWindow + * + * Setting a width and height property on the ApplicationWindow + * will set its initial size, but it won't set it as an automatically binding. + * to resize programmatically the ApplicationWindow they need to + * be assigned again in an imperative fashion + * + * Example usage: + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.ApplicationWindow { + * [...] + * globalDrawer: Kirigami.GlobalDrawer { + * actions: [ + * Kirigami.Action { + * text: "View" + * icon.name: "view-list-icons" + * Kirigami.Action { + * text: "action 1" + * } + * Kirigami.Action { + * text: "action 2" + * } + * Kirigami.Action { + * text: "action 3" + * } + * }, + * Kirigami.Action { + * text: "Sync" + * icon.name: "folder-sync" + * } + * ] + * } + * + * contextDrawer: Kirigami.ContextDrawer { + * id: contextDrawer + * } + * + * pageStack.initialPage: Kirigami.Page { + * mainAction: Kirigami.Action { + * icon.name: "edit" + * onTriggered: { + * // do stuff + * } + * } + * contextualActions: [ + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * }, + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * } + * ] + * [...] + * } + * [...] + * } + * @endcode + * +*/ +Kirigami.AbstractApplicationWindow { + id: root + + /** + * @brief This property holds the stack used to allocate the pages and to + * manage the transitions between them. + * + * It's using a PageRow, while having the same API as PageStack, + * it positions the pages as adjacent columns, with as many columns + * as can fit in the screen. An handheld device would usually have a single + * fullscreen column, a tablet device would have many tiled columns. + * + * @property org::kde::kirigami::PageRow pageStack + */ + readonly property alias pageStack: __pageStack + + // Redefined here as here we can know a pointer to PageRow. + // We negate the canBeEnabled check because we don't want to factor in the automatic drawer provided by Kirigami for page actions for our calculations + wideScreen: width >= (root.pageStack.defaultColumnWidth) + ((contextDrawer && !(contextDrawer instanceof Kirigami.ContextDrawer)) ? contextDrawer.width : 0) + (globalDrawer ? globalDrawer.width : 0) + + Component.onCompleted: { + pageStack.currentItem?.forceActiveFocus() + } + + Kirigami.PageRow { + id: __pageStack + globalToolBar.style: Kirigami.ApplicationHeaderStyle.Auto + anchors { + fill: parent + } + + focus: true + } +} diff --git a/src/controls/Card.qml b/src/controls/Card.qml new file mode 100644 index 0000000..23bce66 --- /dev/null +++ b/src/controls/Card.qml @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami +import "private" as P + +/** + * @brief This is the standard layout of a Card. + * + * It is recommended to use this class when the concept of Cards is needed + * in the application. + * + * This Card has default items as header and footer. The header is an + * image that can contain an optional title and icon, accessible via the + * banner grouped property. + * + * The footer will show a series of toolbuttons (and eventual overflow menu) + * representing the actions list accessible with the list property actions. + * It is possible even tough is discouraged to override the footer: + * in this case the actions property shouldn't be used. + * + * @inherit org::kde::kirigami::AbstractCard + * @since 2.4 + */ +Kirigami.AbstractCard { + id: root + + /** + * @brief This property holds the clickable actions that will be available in the footer + * of the card. + * + * The actions will be represented by a list of ToolButtons with an optional overflow + * menu, when not all of them will fit in the available Card width. + * + * @property list actions + */ + property list actions + + /** + * @brief This grouped property controls the banner image present in the header. + * + * This grouped property has the following sub-properties: + * * ``source: url``: The source for the image. It understands any URL valid for an Image component. + * * ``titleIcon: string``: The optional icon to put in the banner, either a freedesktop-compatible + * icon name (recommended) or any URL supported by QtQuick.Image. + * * ``title: string``: The title for the banner, shown as contrasting text over the image. + * * ``titleAlignment: Qt::Alignment``: The alignment of the title inside the image. + * default: ``Qt.AlignTop | Qt.AlignLeft`` + * * ``titleLevel: int``: The Kirigami.Heading level for the title, which controls the font size. + * default: ``1``, which is the largest size. + * * ``titleWrapMode: QtQuick.Text::wrapMode``: Whether the header text should be able to wrap. + * default: ``Text.NoWrap`` + * + * It also has the full set of properties that QtQuick.Image has, such as sourceSize and fillMode. + * + * @see org::kde::kirigami::private::BannerImage + * @property Image banner + */ + readonly property alias banner: bannerImage + + Accessible.name: banner.title + + header: Kirigami.Padding { + topPadding: -root.topPadding + root.background.border.width + leftPadding: -root.leftPadding + root.background.border.width + rightPadding: -root.rightPadding + root.background.border.width + bottomPadding: root.contentItem ? 0 : -root.bottomPadding + root.background.border.width + + contentItem: P.BannerImage { + id: bannerImage + + implicitWidth: Layout.preferredWidth + implicitHeight: (source.toString().length > 0 && sourceSize.width > 0 && sourceSize.height > 0 ? width / (sourceSize.width / sourceSize.height) : Layout.minimumHeight) + parent.topPadding + parent.bottomPadding + + readonly property real widthWithBorder: width + root.background.border.width * 2 + readonly property real heightWithBorder: height + root.background.border.width * 2 + readonly property real radiusFromBackground: root.background.radius - root.background.border.width + + corners.topLeftRadius: radiusFromBackground + corners.topRightRadius: radiusFromBackground + corners.bottomLeftRadius: radiusFromBackground + corners.bottomRightRadius: heightWithBorder < root.height ? 0 : radiusFromBackground + + checkable: root.checkable + checked: root.checkable && root.checked + + onToggled: checked => { + root.checked = checked; + root.toggled(checked); + } + } + } + + onHeaderChanged: { + if (!header) { + return; + } + + header.anchors.topMargin = Qt.binding(() => -root.topPadding); + header.anchors.leftMargin = Qt.binding(() => -root.leftPadding); + header.anchors.rightMargin = Qt.binding(() => -root.rightPadding); + header.anchors.bottomMargin = Qt.binding(() => 0); + } + + footer: Kirigami.ActionToolBar { + id: actionsToolBar + actions: root.actions + position: QQC2.ToolBar.Footer + } +} diff --git a/src/controls/CardsLayout.qml b/src/controls/CardsLayout.qml new file mode 100644 index 0000000..1fe00e0 --- /dev/null +++ b/src/controls/CardsLayout.qml @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +/** + * @brief A GridLayout optimized for showing one or two columns of cards, + * depending on the available space. + * + * It Should be used when the cards are not instantiated by a model or by a + * model which has always very few items. + * + * They are presented as a grid of two columns which will remain + * centered if the application is really wide, or become a single + * column if there is not enough space for two columns, + * such as a mobile phone screen. + * + * A CardsLayout should always be contained within a ColumnLayout. + * + * @since 2.4 + * @inherit QtQuick.Layouts.GridLayout + */ +GridLayout { + /** + * @brief This property holds the maximum number of columns. + * + * This layout will never lay out the items in more columns than maximumColumns + * + * default: ``2`` + * + * @since 2.5 + */ + property int maximumColumns: 2 + + /** + * @brief This property holds the maximum width the columns may have. + * + * The cards will never become wider than this size; when the GridLayout is wider than + * maximumColumnWidth, it will switch from one to two columns. + * + * If the default needs to be overridden for some reason, + * it is advised to express this unit as a multiple + * of Kirigami.Units.gridUnit. + * + * default: ``20 * Kirigami.Units.gridUnit`` + */ + property int maximumColumnWidth: Kirigami.Units.gridUnit * 20 + + /** + * @brief This property holds the minimum width the columns may have. + * + * The layout will try to dispose of items + * in a number of columns that will respect this size constraint. + * + * default: ``12 * Kirigami.Units.gridUnit`` + * + * @since 2.5 + */ + property int minimumColumnWidth: Kirigami.Units.gridUnit * 12 + + columns: Math.max(1, Math.min(maximumColumns > 0 ? maximumColumns : Infinity, + Math.floor(width/minimumColumnWidth), + Math.ceil(width/maximumColumnWidth))); + + rowSpacing: Kirigami.Units.largeSpacing + columnSpacing: Kirigami.Units.largeSpacing + + + // NOTE: this default width which defaults to 2 columns is just to remove a binding loop on columns + width: maximumColumnWidth*2 + Kirigami.Units.largeSpacing + // same computation of columns, but on the parent size + Layout.preferredWidth: maximumColumnWidth * Math.max(1, Math.min(maximumColumns > 0 ? maximumColumns : Infinity, + Math.floor(parent.width/minimumColumnWidth), + Math.ceil(parent.width/maximumColumnWidth))) + Kirigami.Units.largeSpacing * (columns - 1) + + Layout.maximumWidth: Layout.preferredWidth + Layout.alignment: Qt.AlignHCenter + + Component.onCompleted: childrenChanged() + onChildrenChanged: { + for (const child of children) { + child.Layout.fillHeight = true; + } + } +} diff --git a/src/controls/CardsListView.qml b/src/controls/CardsListView.qml new file mode 100644 index 0000000..cf8a2e3 --- /dev/null +++ b/src/controls/CardsListView.qml @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +/** + * CardsListView is a ListView which can have AbstractCard as its delegate: it will + * automatically assign the proper spacings and margins around the cards adhering + * to the design guidelines. + * + * CardsListView should be used only with cards which can look good at any + * horizontal size, so it is recommended to directly use AbstractCard with an + * appropriate layout inside, because they are stretching for the whole list width. + * + * Therefore, it is discouraged to use it with the Card type. + * + * The choice between using this view with AbstractCard or a normal ListView + * is purely a choice based on aesthetics alone. + * + * It is recommended to use default values. + * + * @inherit QtQuick.ListView + * @since 2.4 + */ +ListView { + id: root + spacing: Kirigami.Units.largeSpacing * 2 + topMargin: headerPositioning !== ListView.InlineHeader ? spacing : 0 + rightMargin: Kirigami.Units.largeSpacing * 2 + leftMargin: Kirigami.Units.largeSpacing * 2 + reuseItems: true + + headerPositioning: ListView.OverlayHeader + + Keys.onPressed: event => { + if (event.key === Qt.Key_Home) { + positionViewAtBeginning(); + currentIndex = 0; + event.accepted = true; + } + else if (event.key === Qt.Key_End) { + positionViewAtEnd(); + currentIndex = count - 1; + event.accepted = true; + } + } +} diff --git a/src/controls/Chip.qml b/src/controls/Chip.qml new file mode 100644 index 0000000..8f74073 --- /dev/null +++ b/src/controls/Chip.qml @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2022 Felipe Kinoshita +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +import "templates" as KT +import "private" as P + +/** + * @brief A compact element that represents an attribute, action, or filter. + * + * Should be used in a group of multiple elements. e.g when displaying tags in a image viewer. + * + * Example usage: + * * @code + * import org.kde.kirigami as Kirigami + * + * Flow { + * Repeater { + * model: chipsModel + * + * Kirigami.Chip { + * text: model.text + * icon.name: "tag-symbolic" + * closable: model.closable + * onClicked: { + * [...] + * } + * onRemoved: { + * [...] + * } + * } + * } + * } + * @endcode + * + * @since 2.19 + */ +KT.Chip { + id: chip + + implicitWidth: layout.implicitWidth + implicitHeight: toolButton.implicitHeight + + checkable: !closable + hoverEnabled: true + + /** + * @brief This property holds the label item; used for accessing the usual Text properties. + * @property QtQuick.Controls.Label labelItem + */ + property alias labelItem: label + + contentItem: RowLayout { + id: layout + spacing: 0 + + Kirigami.Icon { + id: icon + visible: icon.valid + Layout.preferredWidth: Kirigami.Units.iconSizes.small + Layout.preferredHeight: Kirigami.Units.iconSizes.small + Layout.leftMargin: Kirigami.Units.smallSpacing + color: chip.icon.color + isMask: chip.iconMask + source: chip.icon.name || chip.icon.source + } + QQC2.Label { + id: label + Layout.fillWidth: true + Layout.minimumWidth: Kirigami.Units.gridUnit * 1.5 + Layout.leftMargin: icon.visible ? Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing + Layout.rightMargin: chip.closable ? Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + text: chip.text + color: Kirigami.Theme.textColor + elide: Text.ElideRight + } + QQC2.ToolButton { + id: toolButton + visible: chip.closable + text: qsTr("Remove Tag") + icon.name: "edit-delete-remove" + icon.width: Kirigami.Units.iconSizes.sizeForLabels + icon.height: Kirigami.Units.iconSizes.sizeForLabels + display: QQC2.AbstractButton.IconOnly + onClicked: chip.removed() + } + } + + background: P.DefaultChipBackground {} +} diff --git a/src/controls/ContextDrawer.qml b/src/controls/ContextDrawer.qml new file mode 100644 index 0000000..5232bb7 --- /dev/null +++ b/src/controls/ContextDrawer.qml @@ -0,0 +1,182 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami +import "private" as KP + +/** + * A specialized type of drawer that will show a list of actions + * relevant to the application's current page. + * + * Example usage: + * + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.ApplicationWindow { + * contextDrawer: Kirigami.ContextDrawer { + * enabled: true + * actions: [ + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * }, + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * } + * ] + * } + * } + * @endcode + * + * @inherit OverlayDrawer + */ +Kirigami.OverlayDrawer { + id: root + + handleClosedIcon.source: null + handleOpenIcon.source: null + + /** + * @brief A title for the action list that will be shown to the user when opens the drawer + * + * default: ``qsTr("Actions")`` + */ + property string title: qsTr("Actions") + + /** + * List of contextual actions to be displayed in a ListView. + * + * @see QtQuick.Action + * @see org::kde::kirigami::Action + * @property list actions + */ + property list actions + + /** + * @brief Arbitrary content to show above the list view. + * + * default: `an Item containing a Kirigami.Heading that displays a title whose text is + * controlled by the title property.` + * + * @property Component header + * @since 2.7 + */ + property alias header: menu.header + + /** + * @brief Arbitrary content to show below the list view. + * @property Component footer + * @since 2.7 + */ + property alias footer: menu.footer + + // Not stored in a property, so we don't have to waste memory on an extra list. + function visibleActions() { + return actions.filter( + action => !(action instanceof Kirigami.Action) || action.visible + ); + } + + // Disable for empty menus or when we have a global toolbar + enabled: { + const pageStack = typeof applicationWindow !== "undefined" ? applicationWindow().pageStack : null; + const itemExistsButStyleIsNotToolBar = item => item && item.globalToolBarStyle !== Kirigami.ApplicationHeaderStyle.ToolBar; + return menu.count > 0 + && (!pageStack + || !pageStack.globalToolBar + || (pageStack.layers.depth > 1 + && itemExistsButStyleIsNotToolBar(pageStack.layers.currentItem)) + || itemExistsButStyleIsNotToolBar(pageStack.trailingVisibleItem)); + } + + edge: Qt.application.layoutDirection === Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge + drawerOpen: false + + // list items go to edges, have their own padding + topPadding: 0 + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + + property bool handleVisible: { + if (typeof applicationWindow === "function") { + const w = applicationWindow(); + if (w) { + return w.controlsVisible; + } + } + // For a ContextDrawer its handle is hidden by default + return false; + } + + contentItem: QQC2.ScrollView { + // this just to create the attached property + Kirigami.Theme.inherit: true + implicitWidth: Kirigami.Units.gridUnit * 20 + ListView { + id: menu + interactive: contentHeight > height + + model: root.visibleActions() + + topMargin: root.handle.y > 0 ? menu.height - menu.contentHeight : 0 + header: QQC2.ToolBar { + height: pageStack.globalToolBar.preferredHeight + width: parent.width + + Kirigami.Heading { + id: heading + elide: Text.ElideRight + text: root.title + + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + right: parent.right + leftMargin: Kirigami.Units.largeSpacing + rightMargin: Kirigami.Units.largeSpacing + } + } + } + + delegate: Column { + id: delegate + + required property T.Action modelData + + width: parent.width + + KP.ContextDrawerActionItem { + tAction: delegate.modelData + width: parent.width + } + + Repeater { + model: delegate.modelData instanceof Kirigami.Action && delegate.modelData.expandible + ? delegate.modelData.children : null + + delegate: KP.ContextDrawerActionItem { + width: parent.width + leftPadding: Kirigami.Units.gridUnit + opacity: !root.collapsed + } + } + } + } + } +} diff --git a/src/controls/ContextualHelpButton.qml b/src/controls/ContextualHelpButton.qml new file mode 100644 index 0000000..d98db0a --- /dev/null +++ b/src/controls/ContextualHelpButton.qml @@ -0,0 +1,92 @@ +/* + SPDX-FileCopyrightText: 2020 Felix Ernst + SPDX-FileCopyrightText: 2024 Nate Graham + SPDX-FileCopyrightText: 2024 ivan tkachenko + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +/** + * @brief An inline help button that shows a tooltip when clicked. + * + * Use this component when you want to explain details or usage of a feature of + * the UI, but the explanation is too long to fit in an inline label, and too + * important to put in a hover tooltip and risk the user missing it. + * + * @image html ContextualHelpButton.png "Example of ContextualHelpButton usage" + * + * Example usage: + * @code{.qml} + * import QtQuick.Controls as QQC2 + * import QtQuick.Layouts + * import org.kde.kirigami as Kirigami + * + * RowLayout { + * spacing: Kirigami.Units.smallSpacing + * + * QQC2.CheckBox { + * text: i18n("Allow screen tearing in fullscreen windows") + * } + * + * Kirigami.ContextualHelpButton { + * toolTipText: i18n("With most displays, screen tearing reduces latency at the cost of some visual fidelity at high framerates. Note that not all graphics drivers support this setting.") + * } + * } + * + * @endcode + */ + +QQC2.ToolButton { + id: root + + property alias toolTipText: toolTip.text + property bool toolTipVisible: false + + text: qsTr("Show Contextual Help") + icon.name: "help-contextual-symbolic" + display: QQC2.ToolButton.IconOnly + + Accessible.description: toolTipText + + onReleased: { + toolTip.delay = toolTipVisible ? Kirigami.Units.toolTipDelay : 0; + toolTipVisible = !toolTipVisible; + } + onActiveFocusChanged: { + toolTip.delay = Kirigami.Units.toolTipDelay; + toolTipVisible = false; + } + Layout.maximumHeight: parent?.height ?? -1 + + QQC2.ToolTip { + id: toolTip + clip: true + visible: root.hovered || root.toolTipVisible || toolTipHandler.hovered + onVisibleChanged: { + if (!visible && root.toolTipVisible) { + root.toolTipVisible = false; + delay = Kirigami.Units.toolTipDelay; + } + } + timeout: -1 // Don't disappear while the user might still be reading it! + + HoverHandler { + // Also keep the tooltip open while hovering it + // Fixes the flickering when the popup covers the button + id: toolTipHandler + enabled: !root.toolTipVisible // Only if activated by hovering + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.WhatsThisCursor + acceptedButtons: Qt.NoButton + } +} diff --git a/src/controls/FlexColumn.qml b/src/controls/FlexColumn.qml new file mode 100644 index 0000000..37a5d3d --- /dev/null +++ b/src/controls/FlexColumn.qml @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +//TODO KF6: how much is this used? can be removed? +/** + * @brief FlexColumn is a column that grows in width to a fixed cap. + * @inherit QtQuick.Layouts.ColumnLayout + */ +ColumnLayout { + id: __outer + + default property alias columnChildren: __inner.children + + /** + * @brief This property holds the column's offset from the cross axis. + * + * Note that padding is applied on both sides + * when the column is aligned to a centered cross axis. + * + * default: ``Kirigami.Units.largeSpacing`` + */ + property real padding: Kirigami.Units.largeSpacing + + /** + * @brief This property holds maximum column width. + * + * default: ``Kirigami.Units.gridUnit * 50`` + */ + property real maximumWidth: Kirigami.Units.gridUnit * 50 + + /** + * @brief This property sets column's alignment when it hits its maximum width. + * + * default: ``Qt.AlignHCenter | Qt.AlignTop`` + * + * @property Qt::Alignment alignment + */ + property int alignment: Qt.AlignHCenter | Qt.AlignTop + + /** + * @brief This property holds the inner column's width. + */ + property real innerWidth: __inner.width + + Layout.fillWidth: true + Layout.fillHeight: true + + enum CrossAxis { + Left, + Center, + Right + } + + ColumnLayout { + id: __inner + spacing: __outer.spacing + Layout.maximumWidth: __outer.maximumWidth + Layout.leftMargin: __outer.alignment & Qt.AlignLeft || __outer.alignment & Qt.AlignHCenter ? __outer.padding : 0 + Layout.rightMargin: __outer.alignment & Qt.AlignRight || __outer.alignment & Qt.AlignHCenter ? __outer.padding : 0 + Layout.alignment: __outer.alignment + } +} diff --git a/src/controls/GlobalDrawer.qml b/src/controls/GlobalDrawer.qml new file mode 100644 index 0000000..6b94c08 --- /dev/null +++ b/src/controls/GlobalDrawer.qml @@ -0,0 +1,673 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami +import "private" as KP + +/** + * A specialized form of the Drawer intended for showing an application's + * always-available global actions. Think of it like a mobile version of + * a desktop application's menubar. + * + * Example usage: + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.ApplicationWindow { + * globalDrawer: Kirigami.GlobalDrawer { + * actions: [ + * Kirigami.Action { + * text: "View" + * icon.name: "view-list-icons" + * Kirigami.Action { + * text: "action 1" + * } + * Kirigami.Action { + * text: "action 2" + * } + * Kirigami.Action { + * text: "action 3" + * } + * }, + * Kirigami.Action { + * text: "Sync" + * icon.name: "folder-sync" + * } + * ] + * } + * } + * @endcode + */ +Kirigami.OverlayDrawer { + id: root + + edge: Qt.application.layoutDirection === Qt.RightToLeft ? Qt.RightEdge : Qt.LeftEdge + + handleClosedIcon.source: null + handleOpenIcon.source: null + + handleVisible: { + // When drawer is inline with content and opened, there is no point is showing handle. + if (!modal && drawerOpen) { + return false; + } + + // GlobalDrawer can be hidden by controlsVisible... + if (typeof applicationWindow === "function") { + const w = applicationWindow(); + if (w && !w.controlsVisible) { + return false; + } + } + + // ...but it still performs additional checks. + return !isMenu || Kirigami.Settings.isMobile; + } + + enabled: !isMenu || Kirigami.Settings.isMobile + +//BEGIN properties + /** + * @brief This property holds the title displayed at the top of the drawer. + * @see org::kde::kirigami::private::BannerImage::title + * @property string title + */ + property string title + + /** + * @brief This property holds an icon to be displayed alongside the title. + * @see org::kde::kirigami::private::BannerImage::titleIcon + * @see org::kde::kirigami::Icon::source + * @property var titleIcon + */ + property var titleIcon + + /** + * @brief This property holds the actions displayed in the drawer. + * + * The list of actions can be nested having a tree structure. + * A tree depth bigger than 2 is discouraged. + * + * Example usage: + * + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.ApplicationWindow { + * globalDrawer: Kirigami.GlobalDrawer { + * actions: [ + * Kirigami.Action { + * text: "View" + * icon.name: "view-list-icons" + * Kirigami.Action { + * text: "action 1" + * } + * Kirigami.Action { + * text: "action 2" + * } + * Kirigami.Action { + * text: "action 3" + * } + * }, + * Kirigami.Action { + * text: "Sync" + * icon.name: "folder-sync" + * } + * ] + * } + * } + * @endcode + * @property list actions + */ + property list actions + + /** + * @brief This property holds an item that will always be displayed at the top of the drawer. + * + * If the drawer contents can be scrolled, this item will stay still and won't scroll. + * + * @note This property is mainly intended for toolbars. + * @since 2.12 + */ + property alias header: mainLayout.header + + /** + * @brief This property holds an item that will always be displayed at the bottom of the drawer. + * + * If the drawer contents can be scrolled, this item will stay still and won't scroll. + * + * @note This property is mainly intended for toolbars. + * @since 6.0 + */ + property alias footer: mainLayout.footer + + /** + * @brief This property holds items that are displayed above the actions. + * + * Example usage: + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.ApplicationWindow { + * [...] + * globalDrawer: Kirigami.GlobalDrawer { + * actions: [...] + * topContent: [Button { + * text: "Button" + * onClicked: //do stuff + * }] + * } + * [...] + * } + * @endcode + * @property list topContent + */ + property alias topContent: topContent.data + + /** + * @brief This property holds items that are displayed under the actions. + * + * Example usage: + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.ApplicationWindow { + * [...] + * globalDrawer: Kirigami.GlobalDrawer { + * actions: [...] + * Button { + * text: "Button" + * onClicked: //do stuff + * } + * } + * [...] + * } + * @endcode + * @note This is a `default` property. + * @property list content + */ + default property alias content: mainContent.data + + /** + * @brief This property sets whether content items at the top should be shown. + * when the drawer is collapsed as a sidebar. + * + * If you want to keep some items visible and some invisible, set this to + * false and control the visibility/opacity of individual items, + * binded to the collapsed property + * + * default: ``false`` + * + * @since 2.5 + */ + property bool showTopContentWhenCollapsed: false + + /** + * @brief This property sets whether content items at the bottom should be shown. + * when the drawer is collapsed as a sidebar. + * + * If you want to keep some items visible and some invisible, set this to + * false and control the visibility/opacity of individual items, + * binded to the collapsed property + * + * default: ``false`` + * + * @see content + * @since 2.5 + */ + property bool showContentWhenCollapsed: false + + // TODO + property bool showHeaderWhenCollapsed: false + + /** + * @brief This property sets whether activating a leaf action resets the + * menu to show leaf's parent actions. + * + * A leaf action is an action without any child actions. + * + * default: ``true`` + */ + property bool resetMenuOnTriggered: true + + /** + * @brief This property points to the action acting as a submenu + */ + readonly property T.Action currentSubMenu: stackView.currentItem?.current ?? null + + /** + * @brief This property sets whether the drawer becomes a menu on the desktop. + * + * default: ``false`` + * + * @since 2.11 + */ + property bool isMenu: false + + /** + * @brief This property sets the visibility of the collapse button + * when the drawer collapsible. + * + * default: ``true`` + * + * @since 2.12 + */ + property bool collapseButtonVisible: true +//END properties + + /** + * @brief This function reverts the menu back to its initial state + */ + function resetMenu() { + stackView.pop(stackView.get(0, T.StackView.DontLoad)); + if (root.modal) { + root.drawerOpen = false; + } + } + + //BEGIN FUNCTIONS + /** + * @brief This method checks whether a particular drawer entry is in view, and scrolls + * the drawer to center the item if it is not. + * + * Drawer items supplied through the actions property will handle this automatically, + * but items supplied in topContent will need to call this explicitly on receiving focus + * Otherwise, if the user passes focus to the item with e.g. keyboard navigation, it may + * be outside the visible area. + * + * When called, this method will place the visible area such that the item at the + * center if any part of it is currently outside. + * + * @code + * QQC2.ItemDelegate { + * id: item + * // ... + * onFocusChanged: if (focus) drawer.ensureVisible(item) + * } + * @endcode + * + * @param item The item that should be in the visible area of the drawer. Item coordinates need to be in the coordinate system of the drawer's flickable. + * @param yOffset Offset to align the item's and the flickable's coordinate system (optional) + */ + //END FUNCTIONS + + function ensureVisible(item: Item, yOffset: int) { + var actualItemY = item.y + (yOffset ?? 0) + var viewYPosition = (item.height <= mainFlickable.height) + ? Math.round(actualItemY + item.height / 2 - mainFlickable.height / 2) + : actualItemY + if (actualItemY < mainFlickable.contentY) { + mainFlickable.contentY = Math.max(0, viewYPosition) + } else if ((actualItemY + item.height) > (mainFlickable.contentY + mainFlickable.height)) { + mainFlickable.contentY = Math.min(mainFlickable.contentHeight - mainFlickable.height, viewYPosition) + } + mainFlickable.returnToBounds() + } + + // rightPadding: !Kirigami.Settings.isMobile && mainFlickable.contentHeight > mainFlickable.height ? Kirigami.Units.gridUnit : Kirigami.Units.smallSpacing + + Kirigami.Theme.colorSet: modal ? Kirigami.Theme.Window : Kirigami.Theme.View + + onIsMenuChanged: drawerOpen = false + + Component { + id: menuComponent + + Column { + property alias model: actionsRepeater.model + property T.Action current + property int level: 0 + + spacing: 0 + Layout.maximumHeight: Layout.minimumHeight + + QQC2.ItemDelegate { + id: backItem + + visible: level > 0 + width: parent.width + icon.name: mirrored ? "go-previous-symbolic-rtl" : "go-previous-symbolic" + + text: Kirigami.MnemonicData.richTextLabel + Accessible.name: Kirigami.MnemonicData.plainTextLabel + activeFocusOnTab: true + + Kirigami.MnemonicData.enabled: enabled && visible + Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.MenuItem + Kirigami.MnemonicData.label: qsTr("Back") + + onClicked: stackView.pop() + + Keys.onEnterPressed: stackView.pop() + Keys.onReturnPressed: stackView.pop() + + Keys.onDownPressed: nextItemInFocusChain().focus = true + Keys.onUpPressed: nextItemInFocusChain(false).focus = true + } + + Shortcut { + sequence: backItem.Kirigami.MnemonicData.sequence + onActivated: backItem.clicked() + } + + Repeater { + id: actionsRepeater + + readonly property bool withSections: { + for (const action of root.actions) { + if (action.hasOwnProperty("expandible") && action.expandible) { + return true; + } + } + return false; + } + + model: root.actions + + delegate: ActionDelegate { + required property T.Action modelData + + tAction: modelData + withSections: actionsRepeater.withSections + } + } + } + } + + component ActionDelegate : Column { + id: delegate + + required property int index + required property T.Action tAction + required property bool withSections + + // `as` case operator is still buggy + readonly property Kirigami.Action kAction: tAction instanceof Kirigami.Action ? tAction : null + + readonly property bool isExpanded: { + return !root.collapsed + && kAction + && kAction.expandible + && kAction.children.length > 0; + } + + visible: kAction?.visible ?? true + + width: parent.width + + KP.GlobalDrawerActionItem { + Kirigami.Theme.colorSet: !root.modal && !root.collapsed && delegate.withSections + ? Kirigami.Theme.Window : parent.Kirigami.Theme.colorSet + + visible: !delegate.isExpanded + width: parent.width + + tAction: delegate.tAction + + onCheckedChanged: { + // move every checked item into view + if (checked && topContent.height + backItem.height + (delegate.index + 1) * height - mainFlickable.contentY > mainFlickable.height) { + mainFlickable.contentY += height + } + } + + onFocusChanged: { + if (focus) { + root.ensureVisible (delegate, topContent.height + (backItem.visible ? backItem.height : 0)) + } + } + } + + Item { + id: headerItem + + visible: delegate.isExpanded + height: sectionHeader.implicitHeight + width: parent.width + + Kirigami.ListSectionHeader { + id: sectionHeader + + anchors.fill: parent + Kirigami.Theme.colorSet: root.modal ? Kirigami.Theme.View : Kirigami.Theme.Window + + contentItem: RowLayout { + spacing: sectionHeader.spacing + + Kirigami.Icon { + property int size: Kirigami.Units.iconSizes.smallMedium + Layout.minimumHeight: size + Layout.maximumHeight: size + Layout.minimumWidth: size + Layout.maximumWidth: size + source: delegate.tAction.icon.name || delegate.tAction.icon.source + } + + Kirigami.Heading { + level: 4 + text: delegate.tAction.text + elide: Text.ElideRight + Layout.fillWidth: true + } + } + } + } + + Repeater { + model: delegate.isExpanded ? (delegate.kAction?.children ?? null) : null + + NestedActionDelegate { + required property T.Action modelData + + tAction: modelData + withSections: delegate.withSections + } + } + } + + component NestedActionDelegate : KP.GlobalDrawerActionItem { + required property bool withSections + + width: parent.width + opacity: !root.collapsed + leftPadding: withSections && !root.collapsed && !root.modal ? padding * 2 : padding * 4 + } + + contentItem: Kirigami.HeaderFooterLayout { + id: mainLayout + + anchors { + fill: parent + topMargin: root.collapsed && !showHeaderWhenCollapsed ? -contentItem.y : 0 + } + + Behavior on anchors.topMargin { + NumberAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + + header: RowLayout { + visible: root.title.length > 0 || Boolean(root.titleIcon) + spacing: Kirigami.Units.largeSpacing + + Kirigami.Icon { + source: root.titleIcon + } + + Kirigami.Heading { + text: root.title + elide: Text.ElideRight + visible: !root.collapsed + Layout.fillWidth: true + } + } + + contentItem: QQC2.ScrollView { + id: scrollView + + //ensure the attached property exists + Kirigami.Theme.inherit: true + + // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890 + QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff + + implicitWidth: Math.min(Kirigami.Units.gridUnit * 20, root.parent.width * 0.8) + + Flickable { + id: mainFlickable + + contentWidth: width + contentHeight: mainColumn.Layout.minimumHeight + + clip: (mainLayout.header?.visible ?? false) || (mainLayout.footer?.visible ?? false) + + ColumnLayout { + id: mainColumn + width: mainFlickable.width + spacing: 0 + height: Math.max(scrollView.height, Layout.minimumHeight) + + ColumnLayout { + id: topContent + + spacing: 0 + + Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: root.leftPadding + Layout.rightMargin: root.rightPadding + Layout.bottomMargin: Kirigami.Units.smallSpacing + Layout.topMargin: root.topPadding + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: implicitHeight * opacity + // NOTE: why this? just Layout.fillWidth: true doesn't seem sufficient + // as items are added only after this column creation + Layout.minimumWidth: parent.width - root.leftPadding - root.rightPadding + + visible: children.length > 0 && childrenRect.height > 0 && opacity > 0 + opacity: !root.collapsed || showTopContentWhenCollapsed + + Behavior on opacity { + // not an animator as is binded + NumberAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + + T.StackView { + id: stackView + + property KP.ActionsMenu openSubMenu + + clip: true + Layout.fillWidth: true + Layout.minimumHeight: currentItem ? currentItem.implicitHeight : 0 + Layout.maximumHeight: Layout.minimumHeight + + initialItem: menuComponent + + // NOTE: it's important those are NumberAnimation and not XAnimators + // as while the animation is running the drawer may close, and + // the animator would stop when not drawing see BUG 381576 + popEnter: Transition { + NumberAnimation { property: "x"; from: (stackView.mirrored ? -1 : 1) * -stackView.width; to: 0; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.OutCubic } + } + + popExit: Transition { + NumberAnimation { property: "x"; from: 0; to: (stackView.mirrored ? -1 : 1) * stackView.width; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.OutCubic } + } + + pushEnter: Transition { + NumberAnimation { property: "x"; from: (stackView.mirrored ? -1 : 1) * stackView.width; to: 0; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.OutCubic } + } + + pushExit: Transition { + NumberAnimation { property: "x"; from: 0; to: (stackView.mirrored ? -1 : 1) * -stackView.width; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.OutCubic } + } + + replaceEnter: Transition { + NumberAnimation { property: "x"; from: (stackView.mirrored ? -1 : 1) * stackView.width; to: 0; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.OutCubic } + } + + replaceExit: Transition { + NumberAnimation { property: "x"; from: 0; to: (stackView.mirrored ? -1 : 1) * -stackView.width; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.OutCubic } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: root.actions.length > 0 + Layout.minimumHeight: Kirigami.Units.smallSpacing + } + + ColumnLayout { + id: mainContent + Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: root.leftPadding + Layout.rightMargin: root.rightPadding + Layout.fillWidth: true + Layout.fillHeight: true + // NOTE: why this? just Layout.fillWidth: true doesn't seem sufficient + // as items are added only after this column creation + Layout.minimumWidth: parent.width - root.leftPadding - root.rightPadding + visible: children.length > 0 && (opacity > 0 || mainContentAnimator.running) + opacity: !root.collapsed || showContentWhenCollapsed + Behavior on opacity { + OpacityAnimator { + id: mainContentAnimator + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + + Item { + Layout.minimumWidth: Kirigami.Units.smallSpacing + Layout.minimumHeight: root.bottomPadding + } + + QQC2.ToolButton { + Layout.fillWidth: true + + icon.name: { + if (root.collapsible && root.collapseButtonVisible) { + // Check for edge regardless of RTL/locale/mirrored status, + // because edge can be set externally. + const mirrored = root.edge === Qt.RightEdge; + + if (root.collapsed) { + return mirrored ? "sidebar-expand-right" : "sidebar-expand-left"; + } else { + return mirrored ? "sidebar-collapse-right" : "sidebar-collapse-left"; + } + } + return ""; + } + + visible: root.collapsible && root.collapseButtonVisible + text: root.collapsed ? "" : qsTr("Close Sidebar") + + onClicked: root.collapsed = !root.collapsed + + QQC2.ToolTip.visible: root.collapsed && (Kirigami.Settings.tabletMode ? pressed : hovered) + QQC2.ToolTip.text: qsTr("Open Sidebar") + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + } + } + } + } + } +} diff --git a/src/controls/Heading.qml b/src/controls/Heading.qml new file mode 100644 index 0000000..80a380d --- /dev/null +++ b/src/controls/Heading.qml @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: 2012 Sebastian Kügler + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +/** + * @brief A heading label used for subsections of texts. + * + * The characteristics of the text will be automatically set according to the + * Kirigami.Theme. Use this components for section titles or headings in your UI, + * for example page or section titles. + * + * Example usage: + * @code + * import org.kde.kirigami as Kirigami + * [...] + * Column { + * Kirigami.Heading { + * text: "Apples in the sunlight" + * level: 2 + * } + * [...] + * } + * @endcode + * + * The most important property is "text", which applies to the text property of + * Label. See the Label component from QtQuick.Controls 2 and primitive QML Text + * element API for additional properties, methods and signals. + * + * @inherit QtQuick.Controls.Label + */ +QQC2.Label { + id: heading + + /** + * @brief This property holds the level of the heading, which determines its size. + * + * This property holds the level, which determines how large the header is. + * + * Acceptable values range from 1 (big) to 5 (small). + * + * default: ``1`` + */ + property int level: 1 + + /** + * @brief This enumeration defines heading types. + * + * This enum helps with heading visibility (making it less or more important). + */ + enum Type { + Normal, + Primary, + Secondary + } + + /** + * @brief This property holds the heading type. + * + * The type of the heading. This can be: + * * ``Kirigami.Heading.Type.Normal``: Create a normal heading (default) + * * ``Kirigami.Heading.Type.Primary``: Makes the heading more prominent. Useful + * when making the heading bigger is not enough. + * * ``Kirigami.Heading.Type.Secondary``: Makes the heading less prominent. + * Useful when an heading is for a less important section in an application. + * + * @property Heading::Type type + * @since 5.82 + */ + property int type: Heading.Type.Normal + + font.pointSize: { + let factor = 1; + switch (heading.level) { + case 1: + factor = 1.35; + break; + case 2: + factor = 1.20; + break; + case 3: + factor = 1.15; + break; + case 4: + factor = 1.10; + break; + default: + break; + } + return Kirigami.Theme.defaultFont.pointSize * factor; + } + font.weight: type === Heading.Type.Primary ? Font.DemiBold : Font.Normal + + opacity: type === Heading.Type.Secondary ? 0.7 : 1 + + Accessible.role: Accessible.Heading +} diff --git a/src/controls/InlineMessage.qml b/src/controls/InlineMessage.qml new file mode 100644 index 0000000..3846a6e --- /dev/null +++ b/src/controls/InlineMessage.qml @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: 2018 Eike Hein + * SPDX-FileCopyrightText: 2018 Marco Martin + * SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami +import org.kde.kirigami.templates as KT + +/** + * An inline message item with support for informational, positive, + * warning and error types, and with support for associated actions. + * + * InlineMessage can be used to give information to the user or + * interact with the user, without requiring the use of a dialog. + * + * The InlineMessage item is hidden by default. It also manages its + * height (and implicitHeight) during an animated reveal when shown. + * You should avoid setting height on an InlineMessage unless it is + * already visible. + * + * Optionally an icon can be set, defaulting to an icon appropriate + * to the message type otherwise. + * + * Optionally a close button can be shown. + * + * Actions are added from left to right. If more actions are set than + * can fit, an overflow menu is provided. + * + * Example usage: + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.InlineMessage { + * type: Kirigami.MessageType.Error + * + * text: i18n("My error message") + * + * actions: [ + * Kirigami.Action { + * icon.name: "list-add" + * text: i18n("Add") + * onTriggered: source => { + * // do stuff + * } + * }, + * Kirigami.Action { + * icon.name: "edit" + * text: i18n("Edit") + * onTriggered: source => { + * // do stuff + * } + * } + * ] + * } + * @endcode + * @inherit org::kde::kirigami::templates::InlineMessage + * @since 5.45 + */ +KT.InlineMessage { + id: root + + // a rectangle padded with anchors.margins is used to simulate a border + leftPadding: bgFillRect.anchors.leftMargin + Kirigami.Units.smallSpacing + topPadding: bgFillRect.anchors.topMargin + Kirigami.Units.smallSpacing + rightPadding: bgFillRect.anchors.rightMargin + Kirigami.Units.smallSpacing + bottomPadding: bgFillRect.anchors.bottomMargin + Kirigami.Units.smallSpacing + + background: Rectangle { + id: bgBorderRect + + color: switch (root.type) { + case Kirigami.MessageType.Positive: return Kirigami.Theme.positiveTextColor; + case Kirigami.MessageType.Warning: return Kirigami.Theme.neutralTextColor; + case Kirigami.MessageType.Error: return Kirigami.Theme.negativeTextColor; + default: return Kirigami.Theme.activeTextColor; + } + + radius: root.position === KT.InlineMessage.Position.Inline ? Kirigami.Units.cornerRadius : 0 + + Rectangle { + id: bgFillRect + + anchors.fill: parent + anchors { + leftMargin: root.position === KT.InlineMessage.Position.Inline ? 1 : 0 + topMargin: root.position === KT.InlineMessage.Position.Header ? 0 : 1 + rightMargin: root.position === KT.InlineMessage.Position.Inline ? 1 : 0 + bottomMargin: root.position === KT.InlineMessage.Position.Footer ? 0 : 1 + } + + color: Kirigami.Theme.backgroundColor + + radius: bgBorderRect.radius * 0.60 + } + + Rectangle { + anchors.fill: bgFillRect + + color: bgBorderRect.color + + opacity: 0.20 + + radius: bgFillRect.radius + } + } +} diff --git a/src/controls/InlineViewHeader.qml b/src/controls/InlineViewHeader.qml new file mode 100644 index 0000000..857a309 --- /dev/null +++ b/src/controls/InlineViewHeader.qml @@ -0,0 +1,155 @@ +/* + * SPDX-FileCopyrightText: 2023 Nate Graham + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami + +/** + * @brief A fancy inline view header showing a title and optional actions. + * + * Designed to be set as the header: property of a ListView or GridView, this + * component provides a fancy inline header suitable for explaining the contents + * of its view to the user in an attractive and standardized way. Actions globally + * relevant to the view can be defined using the actions: property. They will + * appear on the right side of the header as buttons, and collapse into an + * overflow menu when there isn't room to show them all. + * + * The width: property must be manually set to the parent view's width. + * + * Example usage: + * @code{.qml} + * import org.kde.kirigami as Kirigami + * + * ListView { + * id: listView + * + * headerPositioning: ListView.OverlayHeader + * header: InlineViewHeader { + * width: listView.width + * text: "My amazing view" + * actions: [ + * Kirigami.Action { + * icon.name: "list-add-symbolic" + * text: "Add item" + * onTriggered: { + * // do stuff + * } + * } + * ] + * } + * + * model: [...] + * delegate: [...] + * } + * @endcode + * @inherit QtQuick.QQC2.ToolBar + */ +T.ToolBar { + id: root + +//BEGIN properties + /** + * @brief This property holds the title text. + */ + property string text + + /** + * This property holds the list of actions to show on the header. Actions + * are added from left to right. If more actions are set than can fit, an + * overflow menu is provided. + */ + property list actions +//END properties + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + Math.ceil(label.implicitWidth) + + rowLayout.spacing + + Math.ceil(Math.max(buttonsLoader.implicitWidth, buttonsLoader.Layout.minimumWidth)) + + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + topPadding: Kirigami.Units.smallSpacing + (root.position === T.ToolBar.Footer ? separator.implicitHeight : 0) + leftPadding: Kirigami.Units.largeSpacing + rightPadding: Kirigami.Units.smallSpacing + bottomPadding: Kirigami.Units.smallSpacing + (root.position === T.ToolBar.Header ? separator.implicitHeight : 0) + + z: 999 // don't let content overlap it + + // HACK Due to the lack of a GridView.headerPositioning property, + // we need to "stick" ourselves to the top manually by translating Y accordingly. + // see see https://bugreports.qt.io/browse/QTBUG-117035. + // Conveniently, GridView is only attached to the root of the delegate (or headerItem), + // so this will only be done if the InlineViewHeader itself is the header item. + // And of course it won't be there for ListView either, where we have headerPositioning. + transform: Translate { + y: root.GridView.view ? root.GridView.view.contentY + root.height : 0 + } + + background: Rectangle { + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + // We want a color that's basically halfway between the view background + // color and the window background color. But due to the use of color + // scopes, only one will be available at a time. So to get basically the + // same thing, we blend the view background color with a smidgen of the + // text color. + color: Qt.tint(Kirigami.Theme.backgroundColor, Qt.alpha(Kirigami.Theme.textColor, 0.03)) + + Kirigami.Separator { + id: separator + + anchors { + top: root.position === T.ToolBar.Footer ? parent.top : undefined + left: parent.left + right: parent.right + bottom: root.position === T.ToolBar.Header ? parent.bottom : undefined + } + } + } + + contentItem: RowLayout { + id: rowLayout + + spacing: 0 + + Kirigami.Heading { + id: label + + Layout.fillWidth: !buttonsLoader.active + Layout.maximumWidth: { + if (!buttonsLoader.active) { + return -1; + } + return rowLayout.width + - rowLayout.spacing + - buttonsLoader.Layout.minimumWidth; + } + Layout.alignment: Qt.AlignVCenter + level: 2 + text: root.text + elide: Text.ElideRight + wrapMode: Text.NoWrap + maximumLineCount: 1 + } + + Loader { + id: buttonsLoader + + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + Layout.minimumWidth: item?.Layout.minimumWidth ?? 0 + active: root.actions.length > 0 + sourceComponent: Kirigami.ActionToolBar { + actions: root.actions + alignment: Qt.AlignRight + } + } + } +} diff --git a/src/controls/LinkButton.qml b/src/controls/LinkButton.qml new file mode 100644 index 0000000..705fa53 --- /dev/null +++ b/src/controls/LinkButton.qml @@ -0,0 +1,121 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami + +/** + * @brief A button that looks like a link. + * + * It uses the link color settings and triggers an action when clicked. + * + * Maps to the Command Link in the HIG: + * https://develop.kde.org/hig/components/navigation/commandlink/ + * + * @since 5.52 + * @since org.kde.kirigami 2.6 + * @inherit QtQuick.Controls.Label + */ +QQC2.Label { + id: control + + property T.Action action + + /** + * @brief This property holds the mouse buttons that the mouse area reacts to. + * @see QtQuick.MouseArea::acceptedButtons + * @property Qt::MouseButtons acceptedButtons + */ + property alias acceptedButtons: area.acceptedButtons + + /** + * @brief This property holds the mouse area element covering the button. + * @property MouseArea area + */ + property alias mouseArea: area + + /** + * @brief This property holds the normal color of the link when not pressed + * or disabled. + * + * default: Kirigami.Theme.linkColor + * + * @property color normalColor + */ + property color normalColor: Kirigami.Theme.linkColor + + /** + * @brief This property holds the color of the link while pressed. + * + * default: Whatever the normal color is set to, but 200% darker + * + * @property color pressedColor + */ + property color pressedColor: Qt.darker(normalColor) + + /** + * @brief This property holds the color of the link when disabled. + * + * default: Kirigami.Theme.textColor + * + * @property color disabledColor + */ + property color disabledColor: Kirigami.Theme.textColor + + activeFocusOnTab: true + Accessible.role: Accessible.Button + Accessible.name: text + Accessible.onPressAction: clicked({ button: Qt.LeftButton }) + + text: action?.text ?? "" + enabled: action?.enabled ?? true + + onClicked: action?.trigger() + + font.bold: activeFocus + font.underline: enabled + color: if (!enabled) { + return control.disabledColor; + } else if (area.containsPress) { + return control.pressedColor; + } else { + return control.normalColor; + } + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + + signal pressed(var mouse) + signal clicked(var mouse) + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Space: + case Qt.Key_Enter: + case Qt.Key_Return: + case Qt.Key_Select: + control.clicked({ button: Qt.LeftButton }); + event.accepted = true; + break; + case Qt.Key_Menu: + control.pressed({ button: Qt.RightButton }); + event.accepted = true; + break; + } + } + + MouseArea { + id: area + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: mouse => control.clicked(mouse) + onPressed: mouse => control.pressed(mouse) + } +} diff --git a/src/controls/ListItemDragHandle.qml b/src/controls/ListItemDragHandle.qml new file mode 100644 index 0000000..14e9a34 --- /dev/null +++ b/src/controls/ListItemDragHandle.qml @@ -0,0 +1,265 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * SPDX-FileCopyrightText: 2024 Filipe Azevedo + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +/** + * Implements a drag handle supposed to be in items in ListViews to reorder items + * The ListView must visualize a model which supports item reordering, + * such as ListModel.move() or QAbstractItemModel instances with moveRows() correctly implemented. + * In order for ListItemDragHandle to work correctly, the listItem that is being dragged + * should not directly be the delegate of the ListView, but a child of it. + * + * It is recommended to use DelagateRecycler as base delegate like the following code: + * @code + * import QtQuick + * import QtQuick.Layouts + * import QtQuick.Controls as QQC2 + * import org.kde.kirigami as Kirigami + * ... + * Component { + * id: delegateComponent + * QQC2.ItemDelegate { + * id: listItem + * contentItem: RowLayout { + * Kirigami.ListItemDragHandle { + * listItem: listItem + * listView: mainList + * onMoveRequested: (oldIndex, newIndex) => { + * listModel.move(oldIndex, newIndex, 1); + * } + * } + * QQC2.Label { + * text: model.label + * } + * } + * } + * } + * ListView { + * id: mainList + * + * model: ListModel { + * id: listModel + * ListElement { + * label: "Item 1" + * } + * ListElement { + * label: "Item 2" + * } + * ListElement { + * label: "Item 3" + * } + * } + * //this is optional to make list items animated when reordered + * moveDisplaced: Transition { + * YAnimator { + * duration: Kirigami.Units.longDuration + * easing.type: Easing.InOutQuad + * } + * } + * delegate: Loader { + * width: mainList.width + * sourceComponent: delegateComponent + * } + * } + * ... + * @endcode + * + * @since 2.5 + * @inherit QtQuick.Item + */ +Item { + id: root + + /** + * @brief This property holds the delegate that will be dragged around. + * + * This item *must* be a child of the actual ListView's delegate. + */ + property Item listItem + + /** + * @brief This property holds the ListView that the delegate belong to. + */ + property ListView listView + + /** + * @brief This property holds the fact that we are doing incremental move requests or not + */ + property bool incrementalMoves: true + + /** + * @brief This property holds the fact that the handle is being dragged + */ + readonly property alias dragActive: mouseArea.drag.active + + /** + * @brief This signal is emitted when the drag handle wants to move the item in the model. + * + * The following example does the move in the case a ListModel is used: + * @code + * onMoveRequested: (oldIndex, newIndex) => { + * listModel.move(oldIndex, newIndex, 1); + * } + * @endcode + * @param oldIndex the index the item is currently at + * @param newIndex the index we want to move the item to + */ + signal moveRequested(int oldIndex, int newIndex) + + /** + * @brief This signal is emitted when the drag operation is complete and the item has been + * dropped in the new final position. + * @param oldIndex the index the item is currently at + * @param newIndex the index we want to drop the item to + */ + signal dropped(int oldIndex, int newIndex) + + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: Kirigami.Units.iconSizes.smallMedium + + MouseArea { + id: mouseArea + + anchors.fill: parent + + drag { + target: listItem + axis: Drag.YAxis + minimumY: 0 + } + + cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor + preventStealing: true + + QtObject { + id: _previousMove + + property int oldIndex: -1 + property int newIndex: -1 + + function reset() { + _previousMove.oldIndex = -1; + _previousMove.newIndex = -1; + } + } + + Kirigami.Icon { + id: internal + + anchors.fill: parent + + source: "handle-sort" + opacity: mouseArea.pressed || (!Kirigami.Settings.tabletMode && listItem.hovered) ? 1 : 0.6 + + property int startY + property int mouseDownY + property Item originalParent + property int listItemLastY + property bool draggingUp + + function arrangeItem() { + const newIndex = listView.indexAt(1, listView.contentItem.mapFromItem(listItem, 0, listItem.height / 2).y); + + if (newIndex > -1 && ((incrementalMoves && internal.draggingUp && newIndex < index) || + (incrementalMoves && !internal.draggingUp && newIndex > index) || + (!incrementalMoves && newIndex < listView.count))) { + if (_previousMove.oldIndex === index && _previousMove.newIndex === newIndex) { + return; + } + + _previousMove.oldIndex = index; + _previousMove.newIndex = newIndex; + + root.moveRequested(index, newIndex); + } + } + } + + onPressed: mouse => { + internal.originalParent = listItem.parent; + listItem.parent = listView; + listItem.y = internal.originalParent.mapToItem(listItem.parent, listItem.x, listItem.y).y; + internal.originalParent.z = 99; + internal.startY = listItem.y; + internal.listItemLastY = listItem.y; + internal.mouseDownY = mouse.y; + // while dragging listItem's height could change + // we want a const maximumY during the dragging time + mouseArea.drag.maximumY = listView.height - listItem.height; + } + + onPositionChanged: mouse => { + if (!pressed || listItem.y === internal.listItemLastY) { + return; + } + + internal.draggingUp = listItem.y < internal.listItemLastY + internal.listItemLastY = listItem.y; + + internal.arrangeItem(); + + // autoscroll when the dragging item reaches the listView's top/bottom boundary + scrollTimer.running = (listView.contentHeight > listView.height) + && ((listItem.y === 0 && !listView.atYBeginning) + || (listItem.y === mouseArea.drag.maximumY && !listView.atYEnd)); + } + + onReleased: mouse => dropped() + onCanceled: dropped() + + function dropped() { + listItem.y = internal.originalParent.mapFromItem(listItem, 0, 0).y; + listItem.parent = internal.originalParent; + dropAnimation.running = true; + scrollTimer.running = false; + root.dropped(_previousMove.oldIndex, _previousMove.newIndex); + _previousMove.reset(); + } + + SequentialAnimation { + id: dropAnimation + YAnimator { + target: listItem + from: listItem.y + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + PropertyAction { + target: listItem.parent + property: "z" + value: 0 + } + } + + Timer { + id: scrollTimer + + interval: 50 + repeat: true + + onTriggered: { + if (internal.draggingUp) { + listView.contentY -= Kirigami.Units.gridUnit; + if (listView.atYBeginning) { + listView.positionViewAtBeginning(); + stop(); + } + } else { + listView.contentY += Kirigami.Units.gridUnit; + if (listView.atYEnd) { + listView.positionViewAtEnd(); + stop(); + } + } + internal.arrangeItem(); + } + } + } +} diff --git a/src/controls/ListSectionHeader.qml b/src/controls/ListSectionHeader.qml new file mode 100644 index 0000000..21e45fc --- /dev/null +++ b/src/controls/ListSectionHeader.qml @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2019 Björn Feber + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +/** + * @brief A section delegate for the primitive ListView component. + * + * It's intended to make all listviews look coherent. + * + * Any additional content items will be positioned in a row at the trailing side + * of this component. + * + * Example usage: + * @code + * import QtQuick + * import QtQuick.Controls as QQC2 + * import org.kde.kirigami as Kirigami + * + * ListView { + * section.delegate: Kirigami.ListSectionHeader { + * text: section + * + * QQC2.Button { + * text: "Button 1" + * } + * QQC2.Button { + * text: "Button 2" + * } + * } + * } + * @endcode + */ +QQC2.ItemDelegate { + id: listSection + + /** + * @brief This property sets the text of the ListView's section header. + * @property string label + * @deprecated since 6.2 Use base type's AbstractButton::text property directly + */ + @Deprecated { reason: "Use base type's AbstractButton::text property directly" } + property alias label: listSection.text + + default property alias _contents: rowLayout.data + + hoverEnabled: false + + activeFocusOnTab: false + + // we do not need a background + background: Item {} + + topPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing + + Accessible.role: Accessible.Heading + + contentItem: RowLayout { + id: rowLayout + spacing: Kirigami.Units.largeSpacing + + Kirigami.Heading { + Layout.maximumWidth: rowLayout.width + Layout.alignment: Qt.AlignVCenter + + opacity: 0.7 + level: 5 + type: Kirigami.Heading.Primary + text: listSection.text + elide: Text.ElideRight + + // we override the Primary type's font weight (DemiBold) for Bold for contrast with small text + font.weight: Font.Bold + + Accessible.ignored: true + } + + Kirigami.Separator { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + Accessible.ignored: true + } + } +} diff --git a/src/controls/LoadingPlaceholder.qml b/src/controls/LoadingPlaceholder.qml new file mode 100644 index 0000000..750da85 --- /dev/null +++ b/src/controls/LoadingPlaceholder.qml @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2022 Felipe Kinoshita +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +/** + * @brief A placeholder for loading pages. + * + * Example usage: + * @code{.qml} + * Kirigami.Page { + * Kirigami.LoadingPlaceholder { + * anchors.centerIn: parent + * } + * } + * @endcode + * @code{.qml} + * Kirigami.Page { + * Kirigami.LoadingPlaceholder { + * anchors.centerIn: parent + * determinate: true + * progressBar.value: loadingValue + * } + * } + * @endcode + * @inherit org::kde::kirigami::PlaceholderMessage + */ +Kirigami.PlaceholderMessage { + id: loadingPlaceholder + + /** + * @brief This property holds whether the loading message shows a + * determinate progress bar or not. + * + * This should be true if you want to display the actual + * percentage when it's loading. + * + * default: ``false`` + */ + property bool determinate: false + + /** + * @brief This property holds a progress bar. + * + * This should be used to access the progress bar to change its value. + * + * @property QtQuick.Controls.ProgressBar _progressBar + */ + property alias progressBar: _progressBar + + text: qsTr("Loading…") + + QQC2.ProgressBar { + id: _progressBar + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.maximumWidth: Kirigami.Units.gridUnit * 20 + indeterminate: !determinate + from: 0 + to: 100 + } +} diff --git a/src/controls/NavigationTabBar.qml b/src/controls/NavigationTabBar.qml new file mode 100644 index 0000000..1097545 --- /dev/null +++ b/src/controls/NavigationTabBar.qml @@ -0,0 +1,280 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQml +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami + +/** + * @brief Page navigation tab-bar, used as an alternative to sidebars for 3-5 elements. + * + * Can be combined with secondary toolbars above (if in the footer) to provide page actions. + * + * Example usage: + * @code{.qml} + * import QtQuick + * import org.kde.kirigami as Kirigami + * + * Kirigami.ApplicationWindow { + * title: "Clock" + * + * pageStack.initialPage: worldPage + * + * Kirigami.Page { + * id: worldPage + * title: "World" + * visible: false + * } + * Kirigami.Page { + * id: timersPage + * title: "Timers" + * visible: false + * } + * Kirigami.Page { + * id: stopwatchPage + * title: "Stopwatch" + * visible: false + * } + * Kirigami.Page { + * id: alarmsPage + * title: "Alarms" + * visible: false + * } + * + * footer: Kirigami.NavigationTabBar { + * actions: [ + * Kirigami.Action { + * icon.name: "globe" + * text: "World" + * checked: worldPage.visible + * onTriggered: { + * if (!worldPage.visible) { + * while (pageStack.depth > 0) { + * pageStack.pop(); + * } + * pageStack.push(worldPage); + * } + * } + * }, + * Kirigami.Action { + * icon.name: "player-time" + * text: "Timers" + * checked: timersPage.visible + * onTriggered: { + * if (!timersPage.visible) { + * while (pageStack.depth > 0) { + * pageStack.pop(); + * } + * pageStack.push(timersPage); + * } + * } + * }, + * Kirigami.Action { + * icon.name: "chronometer" + * text: "Stopwatch" + * checked: stopwatchPage.visible + * onTriggered: { + * if (!stopwatchPage.visible) { + * while (pageStack.depth > 0) { + * pageStack.pop(); + * } + * pageStack.push(stopwatchPage); + * } + * } + * }, + * Kirigami.Action { + * icon.name: "notifications" + * text: "Alarms" + * checked: alarmsPage.visible + * onTriggered: { + * if (!alarmsPage.visible) { + * while (pageStack.depth > 0) { + * pageStack.pop(); + * } + * pageStack.push(alarmsPage); + * } + * } + * } + * ] + * } + * } + * @endcode + * + * @see NavigationTabButton + * @since 5.87 + * @since org.kde.kirigami 2.19 + * @inherit QtQuick.Templates.Toolbar + */ + +QQC2.ToolBar { + id: root + +//BEGIN properties + /** + * @brief This property holds the list of actions to be displayed in the toolbar. + */ + property list actions + + /** + * @brief This property holds a subset of visible actions of the list of actions. + * + * An action is considered visible if it is either a Kirigami.Action with + * ``visible`` property set to true, or it is a plain QQC2.Action. + */ + readonly property list visibleActions: actions + // Note: instanceof check implies `!== null` + .filter(action => action instanceof Kirigami.Action + ? action.visible + : action !== null + ) + + /** + * @brief The property holds the maximum width of the toolbar actions, before margins are added. + */ + property real maximumContentWidth: { + const minDelegateWidth = Kirigami.Units.gridUnit * 5; + // Always have at least the width of 5 items, so that small amounts of actions look natural. + return minDelegateWidth * Math.max(visibleActions.length, 5); + } + + /** + * @brief This property holds the index of currently checked tab. + * + * If the index set is out of bounds, or the triggered signal did not change any checked property of an action, the index + * will remain the same. + */ + property int currentIndex: tabGroup.checkedButton && tabGroup.buttons.length > 0 ? tabGroup.checkedButton.tabIndex : -1 + + /** + * @brief This property holds the number of tab buttons. + */ + readonly property int count: tabGroup.buttons.length + + /** + * @brief This property holds the ButtonGroup used to manage the tabs. + */ + readonly property T.ButtonGroup tabGroup: tabGroup + + /** + * @brief This property holds the calculated width that buttons on the tab bar use. + * + * @since 5.102 + */ + property real buttonWidth: { + // Counting buttons because Repeaters can be counted among visibleChildren + let visibleButtonCount = 0; + const minWidth = contentItem.height * 0.75; + for (const visibleChild of contentItem.visibleChildren) { + if (contentItem.width / visibleButtonCount >= minWidth && // make buttons go off the screen if there is physically no room for them + visibleChild instanceof T.AbstractButton) { // Checking for AbstractButtons because any AbstractButton can act as a tab + ++visibleButtonCount; + } + } + + return Math.round(contentItem.width / visibleButtonCount); + } +//END properties + + onCurrentIndexChanged: { + if (currentIndex === -1) { + if (tabGroup.checkState !== Qt.Unchecked) { + tabGroup.checkState = Qt.Unchecked; + } + return; + } + if (!tabGroup.checkedButton || tabGroup.checkedButton.tabIndex !== currentIndex) { + const buttonForCurrentIndex = tabGroup.buttons[currentIndex] + if (buttonForCurrentIndex.action) { + // trigger also toggles and causes clicked() to be emitted + buttonForCurrentIndex.action.trigger(); + } else { + // toggle() does not trigger the action, + // so don't use it if you want to use an action. + // It also doesn't cause clicked() to be emitted. + buttonForCurrentIndex.toggle(); + } + } + } + + // ensure that by default, we do not have unintended padding and spacing from the style + spacing: 0 + padding: 0 + topPadding: undefined + leftPadding: undefined + rightPadding: undefined + bottomPadding: undefined + verticalPadding: undefined + // Using Math.round() on horizontalPadding can cause the contentItem to jitter left and right when resizing the window. + horizontalPadding: Math.floor(Math.max(0, width - root.maximumContentWidth) / 2) + + contentWidth: root.maximumContentWidth + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding) + position: { + if (QQC2.ApplicationWindow.window?.footer === root) { + return QQC2.ToolBar.Footer + } else if (parent?.footer === root) { + return QQC2.ToolBar.Footer + } else if (parent?.parent?.footer === parent) { + return QQC2.ToolBar.Footer + } else { + return QQC2.ToolBar.Header + } + } + + contentItem: RowLayout { + id: rowLayout + spacing: root.spacing + } + + // Used to manage which tab is checked and change the currentIndex + T.ButtonGroup { + id: tabGroup + exclusive: true + buttons: root.contentItem.children.filter((child) => child !== instantiator) + + onCheckedButtonChanged: { + if (!checkedButton) { + return + } + if (root.currentIndex !== checkedButton.tabIndex) { + root.currentIndex = checkedButton.tabIndex; + } + } + } + + // Using a Repeater here because Instantiator was causing issues: + // NavigationTabButtons that were supposed to be destroyed were still + // registered as buttons in tabGroup. + // NOTE: This will make Repeater show up as child through visibleChildren + Repeater { + id: instantiator + model: root.visibleActions + delegate: NavigationTabButton { + id: delegate + + required property T.Action modelData + + parent: root.contentItem + action: modelData + // Workaround setting the action when checkable is not explicitly set making tabs uncheckable + onActionChanged: action.checkable = true + + Layout.minimumWidth: root.buttonWidth + Layout.maximumWidth: root.buttonWidth + Layout.fillHeight: true + + Kirigami.Theme.textColor: root.Kirigami.Theme.textColor + Kirigami.Theme.backgroundColor: root.Kirigami.Theme.backgroundColor + Kirigami.Theme.highlightColor: root.Kirigami.Theme.highlightColor + } + } +} diff --git a/src/controls/NavigationTabButton.qml b/src/controls/NavigationTabButton.qml new file mode 100644 index 0000000..c9544ab --- /dev/null +++ b/src/controls/NavigationTabButton.qml @@ -0,0 +1,220 @@ +/* SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami + +/** + * @brief Navigation buttons to be used for the NavigationTabBar component. + * + * It supplies its own padding, and also supports using the QQC2 AbstractButton ``display`` property to be used in column lists. + * + * Alternative way to the "actions" property on NavigationTabBar, as it can be used + * with Repeater to generate buttons from models. + * + * Example usage: + * @code{.qml} + * Kirigami.NavigationTabBar { + * id: navTabBar + * Kirigami.NavigationTabButton { + * visible: true + * icon.name: "document-save" + * text: `test ${tabIndex + 1}` + * QQC2.ButtonGroup.group: navTabBar.tabGroup + * } + * Kirigami.NavigationTabButton { + * visible: false + * icon.name: "document-send" + * text: `test ${tabIndex + 1}` + * QQC2.ButtonGroup.group: navTabBar.tabGroup + * } + * actions: [ + * Kirigami.Action { + * visible: true + * icon.name: "edit-copy" + * icon.height: 32 + * icon.width: 32 + * text: `test 3` + * checked: true + * }, + * Kirigami.Action { + * visible: true + * icon.name: "edit-cut" + * text: `test 4` + * checkable: true + * }, + * Kirigami.Action { + * visible: false + * icon.name: "edit-paste" + * text: `test 5` + * }, + * Kirigami.Action { + * visible: true + * icon.source: "../logo.png" + * text: `test 6` + * checkable: true + * } + * ] + * } + * @endcode + * + * @since 5.87 + * @since org.kde.kirigami 2.19 + * @inherit QtQuick.Templates.TabButton + */ +T.TabButton { + id: control + + /** + * @brief This property tells the index of this tab within the tab bar. + */ + readonly property int tabIndex: { + let tabIdx = 0 + for (const child of parent.children) { + if (child === this) { + return tabIdx + } + // Checking for AbstractButtons because any AbstractButton can act as a tab + if (child instanceof T.AbstractButton) { + ++tabIdx + } + } + return -1 + } + + // FIXME: all those internal properties should go, and the button should style itself in a more standard way + // probably similar to view items + readonly property color __foregroundColor: Kirigami.Theme.textColor + readonly property color __highlightForegroundColor: Kirigami.Theme.textColor + + readonly property color __pressedColor: Qt.alpha(Kirigami.Theme.highlightColor, 0.3) + readonly property color __hoverSelectColor: Qt.alpha(Kirigami.Theme.highlightColor, 0.2) + readonly property color __checkedBorderColor: Qt.alpha(Kirigami.Theme.highlightColor, 0.7) + readonly property color __pressedBorderColor: Qt.alpha(Kirigami.Theme.highlightColor, 0.9) + + readonly property real __verticalMargins: (display === T.AbstractButton.TextBesideIcon) ? Kirigami.Units.largeSpacing : 0 + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + display: T.AbstractButton.TextUnderIcon + + Kirigami.Theme.colorSet: Kirigami.Theme.Window + Kirigami.Theme.inherit: false + + hoverEnabled: true + + padding: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing + + icon.height: display === T.AbstractButton.TextBesideIcon ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.smallMedium + icon.width: display === T.AbstractButton.TextBesideIcon ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.smallMedium + icon.color: checked ? __highlightForegroundColor : __foregroundColor + + Kirigami.MnemonicData.enabled: enabled && visible + Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.MenuItem + Kirigami.MnemonicData.label: text + + Accessible.description: Kirigami.MnemonicData.plainTextLabel + Accessible.onPressAction: control.action.trigger() + + background: Rectangle { + Kirigami.Theme.colorSet: Kirigami.Theme.Button + Kirigami.Theme.inherit: false + + implicitHeight: (control.display === T.AbstractButton.TextBesideIcon) ? 0 : (Kirigami.Units.gridUnit * 3 + Kirigami.Units.smallSpacing * 2) + + color: "transparent" + + Rectangle { + width: parent.width - Kirigami.Units.largeSpacing + height: parent.height - Kirigami.Units.largeSpacing + anchors.centerIn: parent + + radius: Kirigami.Units.cornerRadius + color: control.down ? control.__pressedColor : (control.checked || control.hovered ? control.__hoverSelectColor : "transparent") + + border.color: control.visualFocus ? control.__checkedBorderColor : (control.down ? control.__pressedBorderColor : color) + border.width: 1 + + Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } } + Behavior on border.color { ColorAnimation { duration: Kirigami.Units.shortDuration } } + } + } + + contentItem: GridLayout { + columnSpacing: 0 + rowSpacing: (label.visible && label.lineCount > 1) ? 0 : control.spacing + + // if this is a row or a column + columns: control.display !== T.AbstractButton.TextBesideIcon ? 1 : 2 + + Kirigami.Icon { + id: icon + source: control.icon.name || control.icon.source + visible: (control.icon.name.length > 0 || control.icon.source.toString().length > 0) && control.display !== T.AbstractButton.TextOnly + color: control.icon.color + + Layout.topMargin: control.__verticalMargins + Layout.bottomMargin: control.__verticalMargins + Layout.leftMargin: (control.display === T.AbstractButton.TextBesideIcon) ? Kirigami.Units.gridUnit : 0 + Layout.rightMargin: (control.display === T.AbstractButton.TextBesideIcon) ? Kirigami.Units.gridUnit : 0 + + Layout.alignment: { + if (control.display === T.AbstractButton.TextBesideIcon) { + // row layout + return Qt.AlignVCenter | Qt.AlignRight; + } else { + // column layout + return Qt.AlignHCenter | ((!label.visible || label.lineCount > 1) ? Qt.AlignVCenter : Qt.AlignBottom); + } + } + implicitHeight: source ? control.icon.height : 0 + implicitWidth: source ? control.icon.width : 0 + + Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } } + } + QQC2.Label { + id: label + + text: control.Kirigami.MnemonicData.richTextLabel + Accessible.name: control.Kirigami.MnemonicData.plainTextLabel + horizontalAlignment: (control.display === T.AbstractButton.TextBesideIcon) ? Text.AlignLeft : Text.AlignHCenter + + visible: control.display !== T.AbstractButton.IconOnly + wrapMode: Text.Wrap + elide: Text.ElideMiddle + color: control.checked ? control.__highlightForegroundColor : control.__foregroundColor + + font.pointSize: !icon.visible && control.display === T.AbstractButton.TextBelowIcon + ? Kirigami.Theme.defaultFont.pointSize * 1.20 // 1.20 is equivalent to level 2 heading + : Kirigami.Theme.defaultFont.pointSize + + Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } } + + Layout.topMargin: control.__verticalMargins + Layout.bottomMargin: control.__verticalMargins + + Layout.alignment: { + if (control.display === T.AbstractButton.TextBesideIcon) { + // row layout + return Qt.AlignVCenter | Qt.AlignLeft; + } else { + // column layout + return icon.visible ? Qt.AlignHCenter | Qt.AlignTop : Qt.AlignCenter; + } + } + + Layout.fillWidth: true + + Accessible.ignored: true + } + } +} diff --git a/src/controls/OverlayDrawer.qml b/src/controls/OverlayDrawer.qml new file mode 100644 index 0000000..7fcd56e --- /dev/null +++ b/src/controls/OverlayDrawer.qml @@ -0,0 +1,236 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami +import "private" as KP +import "templates" as KT + +/** + * Overlay Drawers are used to expose additional UI elements needed for + * small secondary tasks for which the main UI elements are not needed. + * For example in Okular Mobile, an Overlay Drawer is used to display + * thumbnails of all pages within a document along with a search field. + * This is used for the distinct task of navigating to another page. + * + * @inherit org::kde::kirigami::templates::OverlayDrawer + */ +KT.OverlayDrawer { + id: root + +//BEGIN Properties + focus: false + modal: true + drawerOpen: !modal + closePolicy: modal ? T.Popup.CloseOnEscape | T.Popup.CloseOnReleaseOutside : T.Popup.NoAutoClose + handleVisible: interactive && (modal || !drawerOpen) && (typeof(applicationWindow)===typeof(Function) && applicationWindow() ? applicationWindow().controlsVisible : true) + + // FIXME: set to false when it does not lead to blocking closePolicy. + // See Kirigami bug: 454119 + interactive: true + + onPositionChanged: { + if (!modal && !root.peeking && !root.animating) { + position = 1; + } + } + + background: Rectangle { + color: Kirigami.Theme.backgroundColor + + Item { + parent: root.handle + anchors.fill: parent + + Kirigami.ShadowedRectangle { + id: handleGraphics + anchors.centerIn: parent + + Kirigami.Theme.colorSet: parent.parent.handleAnchor && parent.parent.handleAnchor.visible ? parent.parent.handleAnchor.Kirigami.Theme.colorSet : Kirigami.Theme.Button + + Kirigami.Theme.backgroundColor: parent.parent.handleAnchor && parent.parent.handleAnchor.visible ? parent.parent.handleAnchor.Kirigami.Theme.backgroundColor : undefined + + Kirigami.Theme.textColor: parent.parent.handleAnchor && parent.parent.handleAnchor.visible ? parent.parent.handleAnchor.Kirigami.Theme.textColor : undefined + + Kirigami.Theme.inherit: false + color: root.handle.pressed ? Kirigami.Theme.highlightColor : Kirigami.Theme.backgroundColor + + visible: !parent.parent.handleAnchor || !parent.parent.handleAnchor.visible || root.handle.pressed || (root.modal && root.position > 0) + + shadow.color: Qt.rgba(0, 0, 0, root.handle.pressed ? 0.6 : 0.4) + shadow.yOffset: 1 + shadow.size: Kirigami.Units.gridUnit / 2 + + width: Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.smallSpacing * 2 + height: width + radius: Kirigami.Units.cornerRadius + Behavior on color { + ColorAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + Loader { + anchors.centerIn: handleGraphics + width: height + height: Kirigami.Units.iconSizes.smallMedium + + Kirigami.Theme.colorSet: handleGraphics.Kirigami.Theme.colorSet + Kirigami.Theme.backgroundColor: handleGraphics.Kirigami.Theme.backgroundColor + Kirigami.Theme.textColor: handleGraphics.Kirigami.Theme.textColor + + asynchronous: true + + source: { + let edge = root.edge; + if (Qt.application.layoutDirection === Qt.RightToLeft) { + if (edge === Qt.LeftEdge) { + edge = Qt.RightEdge; + } else { + edge = Qt.LeftEdge; + } + } + + if ((root.handleClosedIcon.source || root.handleClosedIcon.name) + && (root.handleOpenIcon.source || root.handleOpenIcon.name)) { + return Qt.resolvedUrl("templates/private/GenericDrawerIcon.qml"); + } else if (edge === Qt.LeftEdge) { + return Qt.resolvedUrl("templates/private/MenuIcon.qml"); + } else if (edge === Qt.RightEdge && root instanceof Kirigami.ContextDrawer) { + return Qt.resolvedUrl("templates/private/ContextIcon.qml"); + } else { + return ""; + } + } + onItemChanged: { + if (item) { + item.drawer = root; + item.color = Qt.binding(() => root.handle.pressed + ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor); + } + } + } + } + + Kirigami.Separator { + id: separator + + LayoutMirroring.enabled: false + + anchors { + top: root.edge === Qt.TopEdge ? parent.bottom : (root.edge === Qt.BottomEdge ? undefined : parent.top) + left: root.edge === Qt.LeftEdge ? parent.right : (root.edge === Qt.RightEdge ? undefined : parent.left) + right: root.edge === Qt.RightEdge ? parent.left : (root.edge === Qt.LeftEdge ? undefined : parent.right) + bottom: root.edge === Qt.BottomEdge ? parent.top : (root.edge === Qt.TopEdge ? undefined : parent.bottom) + topMargin: segmentedSeparator.height + } + + visible: !root.modal + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Header + } + + Item { + id: segmentedSeparator + + // an alternative to segmented style is full height + readonly property bool shouldUseSegmentedStyle: { + if (root.edge !== Qt.LeftEdge && root.edge !== Qt.RightEdge) { + return false; + } + if (root.collapsed) { + return false; + } + // compatible header + const header = root.header ?? null; + if (header instanceof T.ToolBar || header instanceof KT.AbstractApplicationHeader) { + return true; + } + // or compatible content + if (root.contentItem instanceof ColumnLayout && root.contentItem.children[0] instanceof T.ToolBar) { + return true; + } + return false; + } + + anchors { + top: parent.top + left: separator.left + right: separator.right + } + + height: { + if (root.edge !== Qt.LeftEdge && root.edge !== Qt.RightEdge) { + return 0; + } + if (typeof applicationWindow === "undefined") { + return 0; + } + const window = applicationWindow(); + const globalToolBar = window.pageStack?.globalToolBar; + if (!globalToolBar) { + return 0; + } + + return globalToolBar.preferredHeight; + } + + visible: separator.visible + + Kirigami.Separator { + LayoutMirroring.enabled: false + + anchors { + fill: parent + topMargin: segmentedSeparator.shouldUseSegmentedStyle ? Kirigami.Units.largeSpacing : 0 + bottomMargin: segmentedSeparator.shouldUseSegmentedStyle ? Kirigami.Units.largeSpacing : 0 + } + + Behavior on anchors.topMargin { + NumberAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + + Behavior on anchors.bottomMargin { + NumberAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Header + } + } + + KP.EdgeShadow { + z: -2 + visible: root.modal + edge: root.edge + anchors { + right: root.edge === Qt.RightEdge ? parent.left : (root.edge === Qt.LeftEdge ? undefined : parent.right) + left: root.edge === Qt.LeftEdge ? parent.right : (root.edge === Qt.RightEdge ? undefined : parent.left) + top: root.edge === Qt.TopEdge ? parent.bottom : (root.edge === Qt.BottomEdge ? undefined : parent.top) + bottom: root.edge === Qt.BottomEdge ? parent.top : (root.edge === Qt.TopEdge ? undefined : parent.bottom) + } + + opacity: root.position === 0 ? 0 : 1 + + Behavior on opacity { + NumberAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + } +} diff --git a/src/controls/OverlaySheet.qml b/src/controls/OverlaySheet.qml new file mode 100644 index 0000000..03be40f --- /dev/null +++ b/src/controls/OverlaySheet.qml @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import org.kde.kirigami as Kirigami +import "private" as P +import "templates" as T + +/** + * @brief An overlay sheet that covers the current Page content. + * + * Its contents can be scrolled up or down, scrolling all the way up or + * all the way down, dismisses it. + * Use this for big, modal dialogs or information display, that can't be + * logically done as a new separate Page, even if potentially + * are taller than the screen space. + * @inherit org::kde::kirigami::templates::OverlaySheet + */ +T.OverlaySheet { + id: root + + background: P.DefaultCardBackground { + Kirigami.Theme.colorSet: root.Kirigami.Theme.colorSet + Kirigami.Theme.inherit: false + } +} diff --git a/src/controls/Page.qml b/src/controls/Page.qml new file mode 100644 index 0000000..27ca063 --- /dev/null +++ b/src/controls/Page.qml @@ -0,0 +1,301 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Templates as T +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import "private" as P + +/** + * Page is a container for all the app pages: everything pushed to the + * ApplicationWindow's pageStack should be a Page. + * + * @see ScrollablePage + * For content that should be scrollable, such as ListViews, use ScrollablePage instead. + * @inherit QtQuick.Controls.Page + */ +QQC2.Page { + id: root + +//BEGIN properties + padding: Kirigami.Units.gridUnit + + /** + * @brief If the central element of the page is a Flickable + * (ListView and Gridview as well) you can set it there. + * + * Normally, you wouldn't need to do that, but just use the + * ScrollablePage element instead. + * + * Use this if your flickable has some non standard properties, such as not covering the whole Page. + * + * @see ScrollablePage + */ + property Flickable flickable + + /** + * @brief This property holds the actions for the page. + * + * These actions will be displayed in the toolbar on the desktop and inside + * the ContextDrawer on mobile. + * + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.Page { + * actions: [ + * Kirigami.Action {...}, + * Kirigami.Action {...} + * } + * } + * @endcode + */ + property list actions + + /** + * @brief This property tells us if it is the currently active page. + * + * Specifies if it's the currently selected page in the window's pages row, or if layers + * are used whether this is the topmost item on the layers stack. If the page is + * not attached to either a column view or a stack view, expect this to be true. + * + * @since 2.1 + */ + //TODO KF6: remove this or at least all the assumptions about the internal tree structure of items + // Kirigami.ColumnView.view.parent.parent is the StackView in which the ColumnView is, the condition means "is the ColumnView the current layer of the pagerow" + readonly property bool isCurrentPage: Kirigami.ColumnView.view + ? (Kirigami.ColumnView.index === Kirigami.ColumnView.view.currentIndex && Kirigami.ColumnView.view.parent.parent.currentItem === Kirigami.ColumnView.view.parent) + : (parent && parent instanceof QQC2.StackView + ? parent.currentItem === root + : true) + + /** + * An item which stays on top of every other item in the page, + * if you want to make sure some elements are completely in a + * layer on top of the whole content, parent items to this one. + * It's a "local" version of ApplicationWindow's overlay + * + * @property Item overlay + * @since 2.5 + */ + readonly property alias overlay: overlayItem + + /** + * @brief This holds the icon that represents this page. + * @property var icon + */ + property P.ActionIconGroup icon: P.ActionIconGroup {} + + /** + * @brief Progress of a task this page is doing. + * + * Set to undefined to indicate that there are no ongoing tasks. + * + * default: ``undefined`` + * + * @property real progress + */ + property var progress: undefined + + /** + * @brief The delegate which will be used to draw the page title. + * + * It can be customized to put any kind of Item in there. + * + * @since 2.7 + */ + property Component titleDelegate: Component { + id: defaultTitleDelegate + P.DefaultPageTitleDelegate { + text: root.title + } + } + + /** + * The item used as global toolbar for the page + * present only if we are in a PageRow as a page or as a layer, + * and the style is either Titles or ToolBar. + * + * @since 2.5 + */ + readonly property Item globalToolBarItem: globalToolBar.item + + /** + * The style for the automatically generated global toolbar: by default the Page toolbar is the one set globally in the PageRow in its globalToolBar.style property. + * A single page can override the application toolbar style for itself. + * It is discouraged to use this, except very specific exceptions, like a chat + * application which can't have controls on the bottom except the text field. + * If the Page is not in a PageRow, by default the toolbar will be invisible, + * so has to be explicitly set to Kirigami.ApplicationHeaderStyle.ToolBar if + * desired to be used in that case. + */ + property int globalToolBarStyle: { + if (globalToolBar.row && !globalToolBar.stack) { + return globalToolBar.row.globalToolBar.actualStyle; + } else if (globalToolBar.stack) { + return Kirigami.ApplicationHeaderStyle.ToolBar; + } else { + return Kirigami.ApplicationHeaderStyle.None; + } + } +//END properties + +//BEGIN signal and signal handlers + /** + * @brief Emitted when the application requests a Back action. + * + * For instance a global "back" shortcut or the Android + * Back button has been pressed. + * The page can manage the back event by itself, + * and if it set event.accepted = true, it will stop the main + * application to manage the back event. + */ + signal backRequested(var event); + + background: Rectangle { + color: Kirigami.Theme.backgroundColor + } + + // FIXME: on material the shadow would bleed over + clip: root.header !== null; + + Component.onCompleted: { + headerChanged(); + parentChanged(root.parent); + globalToolBar.syncSource(); + bottomToolBar.pageComplete = true + } + + onParentChanged: { + if (!parent) { + return; + } + globalToolBar.stack = null; + globalToolBar.row = null; + + if (root.Kirigami.ColumnView.view) { + globalToolBar.row = root.Kirigami.ColumnView.view.__pageRow; + } + if (root.T.StackView.view) { + globalToolBar.stack = root.T.StackView.view; + globalToolBar.row = root.T.StackView.view.parent instanceof Kirigami.PageRow ? root.T.StackView.view.parent : null; + } + if (globalToolBar.row) { + root.globalToolBarStyleChanged.connect(globalToolBar.syncSource); + globalToolBar.syncSource(); + } + } +//END signals and signal handlers + + // in data in order for them to not be considered for contentItem, contentChildren, contentData + data: [ + Item { + id: overlayItem + parent: root + z: 9997 + anchors { + fill: parent + topMargin: globalToolBar.height + } + } + ] + // global top toolbar if we are in a PageRow (in the row or as a layer) + Kirigami.ColumnView.globalHeader: Loader { + id: globalToolBar + z: 9999 + height: item ? item.implicitHeight : 0 + + width: root.width + // NOTE: This is an Item instead of a Kirigami.PageRow as a workaround for QTBUG-120189 + // Once Frameworks can depend from Qt 6.9, this property can be a PageRow again + property Item row + property T.StackView stack + + // don't load async so that on slower devices we don't have the page content height changing while loading in + // otherwise, it looks unpolished and jumpy + asynchronous: false + + visible: active + active: root.parent && root.visible && (root.titleDelegate !== defaultTitleDelegate || root.globalToolBarStyle === Kirigami.ApplicationHeaderStyle.ToolBar || root.globalToolBarStyle === Kirigami.ApplicationHeaderStyle.Titles) + onActiveChanged: { + if (active) { + syncSource(); + } + } + + function syncSource() { + if (root.globalToolBarStyle !== Kirigami.ApplicationHeaderStyle.ToolBar && + root.globalToolBarStyle !== Kirigami.ApplicationHeaderStyle.Titles && + root.titleDelegate !== defaultTitleDelegate) { + sourceComponent = root.titleDelegate; + } else if (active) { + const url = root.globalToolBarStyle === Kirigami.ApplicationHeaderStyle.ToolBar + ? "private/globaltoolbar/ToolBarPageHeader.qml" + : "private/globaltoolbar/TitlesPageHeader.qml"; + // TODO: find container reliably, remove assumption + setSource(Qt.resolvedUrl(url), { + pageRow: Qt.binding(() => row), + page: root, + current: Qt.binding(() => { + if (!row && !stack) { + return true; + } else if (stack) { + return stack; + } else { + return row.currentIndex === root.Kirigami.ColumnView.level; + } + }), + }); + } + } + } + // bottom action buttons + Kirigami.ColumnView.globalFooter: Loader { + id: bottomToolBar + + property T.Page page: root + property bool pageComplete: false + + visible: active + + active: { + // Important! Do not do anything until the page has been + // completed, so we are sure what the globalToolBarStyle is, + // otherwise we risk creating the content and then discarding it. + if (!pageComplete) { + return false; + } + + if ((globalToolBar.row && globalToolBar.row.globalToolBar.actualStyle === Kirigami.ApplicationHeaderStyle.ToolBar) + || root.globalToolBarStyle === Kirigami.ApplicationHeaderStyle.ToolBar + || root.globalToolBarStyle === Kirigami.ApplicationHeaderStyle.None) { + return false; + } + + if (root.actions.length === 0) { + return false; + } + + // Legacy + if (typeof applicationWindow === "undefined") { + return true; + } + + const drawer = applicationWindow() ? applicationWindow()['contextDrawer'] : undefined; + if (Boolean(drawer) && drawer.enabled && drawer.handleVisible) { + return false; + } + + return true; + } + + source: Qt.resolvedUrl("./private/globaltoolbar/ToolBarPageFooter.qml") + } + + Layout.fillWidth: true +} diff --git a/src/controls/PagePoolAction.qml b/src/controls/PagePoolAction.qml new file mode 100644 index 0000000..1ad39d6 --- /dev/null +++ b/src/controls/PagePoolAction.qml @@ -0,0 +1,238 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQml +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami + +/** + * An action used to load Pages coming from a common PagePool + * in a PageRow or QtQuickControls2 StackView. + * + * @see PagePool + */ +Kirigami.Action { + id: root + +//BEGIN properties + /** + * @brief This property holds the url or filename of the page that this action will load. + */ + property string page + + /** + * @brief This property holds the PagePool object used by this PagePoolAction. + * + * PagePool will make sure only one instance of the page identified by the page url will be created and reused. + * PagePool's lastLoaderUrl property will be used to control the mutual exclusivity of the checked + * state of the PagePoolAction instances sharing the same PagePool. + */ + property Kirigami.PagePool pagePool + + /** + * The pageStack property accepts either a Kirigami.PageRow or a QtQuickControls2 StackView. + * The component that will instantiate the pages, which has to work with a stack logic. + * Kirigami.PageRow is recommended, but will work with QtQuicControls2 StackView as well. + * + * default: `bound to ApplicationWindow's global pageStack, which is a PageRow by default` + */ + property Item pageStack: typeof applicationWindow !== 'undefined' ? applicationWindow().pageStack : null + + /** + * @brief This property sets the page in the pageStack after which + * new pages will be pushed. + * + * All pages present after the given basePage will be removed from the pageStack + */ + property T.Page basePage + + /** + * This property holds a function that generate the property values for the created page + * when it is pushed onto the Kirigami.PagePool. + * + * Example usage: + * @code{.qml} + * Kirigami.PagePoolAction { + * text: i18n("Security") + * icon.name: "security-low" + * page: Qt.resolvedUrl("Security.qml") + * initialProperties: { + * return { + * room: root.room + * } + * } + * } + * @endcode + * @property QVariantMap initialProperties + */ + property var initialProperties + + /** + * @brief This property sets whether PagePoolAction will use the layers property + * implemented by the pageStack. + * + * This is intended for use with PageRow layers to allow PagePoolActions to + * push context-specific pages onto the layers stack. + * + * default: ``false`` + * + * @since 5.70 + * @since org.kde.kirigami 2.12 + */ + property bool useLayers: false +//END properties + + /** + * @returns the page item held in the PagePool or null if it has not been loaded yet. + */ + function pageItem(): Item { + return pagePool.pageForUrl(page) + } + + /** + * @returns true if the page has been loaded and placed on pageStack.layers + * and useLayers is true, otherwise returns null. + */ + function layerContainsPage(): bool { + if (!useLayers || !pageStack.hasOwnProperty("layers")) { + return false; + } + + const pageItem = this.pageItem(); + const item = pageStack.layers.find(item => item === pageItem); + return item !== null; + } + + /** + * @returns true if the page has been loaded and placed on the pageStack, + * otherwise returns null. + */ + function stackContainsPage(): bool { + if (useLayers) { + return false; + } + return pageStack.columnView.containsItem(pagePool.pageForUrl(page)); + } + + checkable: true + + onTriggered: { + if (page.length === 0 || !pagePool || !pageStack) { + return; + } + + // User intends to "go back" to this layer. + const pageItem = this.pageItem(); + if (layerContainsPage() && pageItem !== pageStack.layers.currentItem) { + pageStack.layers.replace(pageItem, pageItem); // force pop above + return; + } + + // User intends to "go back" to this page. + if (stackContainsPage()) { + if (pageStack.hasOwnProperty("layers")) { + pageStack.layers.clear(); + } + } + + const stack = useLayers ? pageStack.layers : pageStack + + if (pageItem != null && stack.currentItem == pageItem) { + return; + } + + if (initialProperties && typeof(initialProperties) !== "object") { + console.warn("initialProperties must be of type object"); + return; + } + + if (!stack.hasOwnProperty("pop") || typeof stack.pop !== "function" || !stack.hasOwnProperty("push") || typeof stack.push !== "function") { + return; + } + + if (pagePool.isLocalUrl(page)) { + if (basePage) { + stack.pop(basePage); + + } else if (!useLayers) { + stack.clear(); + } + + stack.push(initialProperties + ? pagePool.loadPageWithProperties(page, initialProperties) + : pagePool.loadPage(page)); + } else { + const callback = item => { + if (basePage) { + stack.pop(basePage); + + } else if (!useLayers) { + stack.clear(); + } + + stack.push(item); + }; + + if (initialProperties) { + pagePool.loadPage(page, initialProperties, callback); + + } else { + pagePool.loadPage(page, callback); + } + } + } + + // Exposing this as a property is required as Action does not have a default property + property QtObject _private: QtObject { + id: _private + + function setChecked(checked) { + root.checked = checked; + } + + function clearLayers() { + root.pageStack.layers.clear(); + } + + property list connections: [ + Connections { + target: root.pageStack + + function onCurrentItemChanged() { + if (root.useLayers) { + if (root.layerContainsPage()) { + _private.clearLayers(); + } + if (root.checkable) { + _private.setChecked(false); + } + + } else { + if (root.checkable) { + _private.setChecked(root.stackContainsPage()); + } + } + } + }, + Connections { + enabled: root.pageStack.hasOwnProperty("layers") + target: root.pageStack.layers + + function onCurrentItemChanged() { + if (root.useLayers && root.checkable) { + _private.setChecked(root.layerContainsPage()); + + } else { + if (root.pageStack.layers.depth === 1 && root.stackContainsPage()) { + _private.setChecked(true); + } + } + } + } + ] + } +} diff --git a/src/controls/PageRow.qml b/src/controls/PageRow.qml new file mode 100644 index 0000000..6f83779 --- /dev/null +++ b/src/controls/PageRow.qml @@ -0,0 +1,1176 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Templates as QT +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import "private/globaltoolbar" as GlobalToolBar +import "templates" as KT + +/** + * PageRow implements a row-based navigation model, which can be used + * with a set of interlinked information pages. Pages are pushed in the + * back of the row and the view scrolls until that row is visualized. + * A PageRow can show a single page or a multiple set of columns, depending + * on the window width: on a phone a single column should be fullscreen, + * while on a tablet or a desktop more than one column should be visible. + * + * @inherits QtQuick.Templates.Control + */ +QT.Control { + id: root + +//BEGIN PROPERTIES + /** + * @brief This property holds the number of pages currently pushed onto the view. + * @property int depth + */ + readonly property alias depth: columnView.count + + /** + * @brief This property holds the last page in the row. + * @property Page lastItem + */ + readonly property Item lastItem: columnView.contentChildren.length > 0 ? columnView.contentChildren[columnView.contentChildren.length - 1] : null + + /** + * @brief This property holds the currently visible/active page. + * + * Because of the ability to display multiple pages, it will hold the currently active page. + * + * @property Page currentItem + */ + readonly property alias currentItem: columnView.currentItem + + /** + * @brief This property holds the index of the currently active page. + * @see currentItem + * @property int currentIndex + */ + property alias currentIndex: columnView.currentIndex + + /** + * @brief This property sets the initial page for this PageRow. + * @property Page initialPage + */ + property var initialPage + + /** + * @brief This property holds the main ColumnView of this Row. + * @property ColumnView contentItem + */ + contentItem: columnView + + /** + * @brief This property holds the ColumnView that this PageRow owns. + * + * Generally, you shouldn't need to change the value of this property. + * + * @property ColumnView columnView + * @since 2.12 + */ + property alias columnView: columnView + + /** + * @brief This property holds the present pages in the PageRow. + * @property list items + * @since 2.6 + */ + readonly property alias items: columnView.contentChildren + + /** + * @brief This property holds all visible pages in the PageRow, + * excluding those which are scrolled away. + * @property list visibleItems + * @since 2.6 + */ + readonly property alias visibleItems: columnView.visibleItems + + /** + * @brief This property holds the first page in the PageRow that is at least partially visible. + * @note Pages before that one (the one contained in the property) will be out of the viewport. + * @see ColumnView::leadingVisibleItem + * @property Item leadingVisibleItem + * @since 2.6 + */ + readonly property alias leadingVisibleItem: columnView.leadingVisibleItem + + /** + * @brief This property holds the last page in the PageRow that is at least partially visible. + * @note Pages after that one (the one contained in the property) will be out of the viewport. + * @see ColumnView::trailingVisibleItem + * @property Item trailingVisibleItem + * @since 2.6 + */ + readonly property alias trailingVisibleItem: columnView.trailingVisibleItem + + /** + * @brief This property holds the default width for a column. + * + * default: ``20 * Kirigami.Units.gridUnit`` + * + * @note Pages can override it using implicitWidth, Layout.fillWidth, Layout.minimumWidth etc. + */ + property int defaultColumnWidth: Kirigami.Units.gridUnit * 20 + + /** + * @brief This property sets whether it is possible to go back/forward + * by swiping with a gesture on the content view. + * + * default: ``true`` + * + * @property bool interactive + */ + property alias interactive: columnView.interactive + + /** + * @brief This property tells whether the PageRow is wide enough to show multiple pages. + * @since 5.37 + */ + readonly property bool wideMode: width >= defaultColumnWidth * 2 && depth >= 2 + + /** + * @brief This property sets whether the separators between pages should be displayed. + * + * default: ``true`` + * + * @property bool separatorVisible + * @since 5.38 + */ + property alias separatorVisible: columnView.separatorVisible + + /** + * @brief This property sets the appearance of an optional global toolbar for the whole PageRow. + * + * It's a grouped property comprised of the following properties: + * * style (``Kirigami.ApplicationHeaderStyle``): can have the following values: + * * ``Auto``: Depending on application formfactor, it can behave automatically like other values, such as a Breadcrumb on mobile and ToolBar on desktop. + * * ``Breadcrumb``: It will show a breadcrumb of all the page titles in the stack, for easy navigation. + * * ``Titles``: Each page will only have its own title on top. + * * ``ToolBar``: Each page will have the title on top together buttons and menus to represent all of the page actions. Not available on Mobile systems. + * * ``None``: No global toolbar will be shown. + * + * * ``actualStyle``: This will represent the actual style of the toolbar; it can be different from style in the case style is Auto. + * * ``showNavigationButtons``: OR flags combination of Kirigami.ApplicationHeaderStyle.ShowBackButton and Kirigami.ApplicationHeaderStyle.ShowForwardButton. + * * ``toolbarActionAlignment: Qt::Alignment``: How to horizontally align the actions when using the ToolBar style. Note that anything but Qt.AlignRight will cause the title to be hidden (default: ``Qt.AlignRight``). + * * ``minimumHeight: int`` Minimum height of the header, which will be resized when scrolling. Only in Mobile mode (default: ``preferredHeight``, sliding but no scaling). + * * ``preferredHeight: int`` The height the toolbar will usually have. + * * ``leftReservedSpace: int, readonly`` How many pixels of extra space are reserved at the left of the page toolbar (typically for navigation buttons or a drawer handle). + * * ``rightReservedSpace: int, readonly`` How many pixels of extra space are reserved at the right of the page toolbar (typically for a drawer handle). + * + * @property org::kde::kirigami::private::globaltoolbar::PageRowGlobalToolBarStyleGroup globalToolBar + * @since 5.48 + */ + readonly property alias globalToolBar: globalToolBar + + /** + * @brief This property assigns a drawer as an internal left sidebar for this PageRow. + * + * In this case, when open and not modal, the drawer contents will be in the same layer as the base pagerow. + * Pushing any other layer on top will cover the sidebar. + * + * @since 5.84 + */ + // TODO KF6: globaldrawer should use actions also used by this sidebar instead of reparenting globaldrawer contents? + property OverlayDrawer leftSidebar + + /** + * @brief This property holds the modal layers. + * + * Sometimes an application needs a modal page that always covers all the rows. + * For instance the full screen image of an image viewer or a settings page. + * + * @property QtQuick.Controls.StackView layers + * @since 5.38 + */ + property alias layers: layersStack + + /** + * @brief This property holds whether to automatically pop pages at the top of the stack if they are not visible. + * + * If a user navigates to a previous page on the stack (ex. pressing back button) and pages above + * it on the stack are not visible, they will be popped if this property is true. + * + * @since 5.101 + */ + property bool popHiddenPages: false +//END PROPERTIES + +//BEGIN FUNCTIONS + /** + * @brief This method pushes a page on the stack. + * + * A single page can be defined as an url, a component, or an object. It can + * also be an array of the above said types, but in that case, the + * properties' array length must match pages' array length or it must be + * empty. Failing to comply with the following rules will make the method + * return null before doing anything. + * + * @param page A single page or an array of pages. + * @param properties A single property object or an array of property + * objects. + * + * @return The new created page (or the last one if it was an array). + */ + function push(page, properties): QT.Page { + if (!pagesLogic.verifyPages(page, properties)) { + console.warn("Pushed pages do not conform to the rules. Please check the documentation."); + console.trace(); + return null + } + + const item = pagesLogic.insertPage_unchecked(currentIndex + 1, page, properties) + currentIndex = depth - 1 + return item + } + + /** + * @brief Pushes a page as a new dialog on desktop and as a layer on mobile. + * + * @param page A single page defined as either a string url, a component or + * an object (which will be reparented). The following page gains + * `closeDialog()` method allowing to make it indistinguishable to + * close/hide it when in desktop or mobile mode. Note that Kiriami supports + * calling `closeDialog()` only once. + * + * @param properties The properties given when initializing the page. + * @param windowProperties The properties given to the initialized window on desktop. + * @return Returns a newly created page. + */ + function pushDialogLayer(page, properties = {}, windowProperties = {}): QT.Page { + if (!pagesLogic.verifyPages(page, properties)) { + console.warn("Page pushed as a dialog or layer does not conform to the rules. Please check the documentation."); + console.trace(); + return null + } + let item; + if (Kirigami.Settings.isMobile) { + if (QQC2.ApplicationWindow.window.width > Kirigami.Units.gridUnit * 40) { + // open as a QQC2.Dialog + const component = pagesLogic.getMobileDialogLayerComponent(); + const dialog = component.createObject(QQC2.Overlay.overlay, { + width: Qt.binding(() => QQC2.ApplicationWindow.window.width - Kirigami.Units.gridUnit * 5), + height: Qt.binding(() => QQC2.ApplicationWindow.window.height - Kirigami.Units.gridUnit * 5), + x: Kirigami.Units.gridUnit * 2.5, + y: Kirigami.Units.gridUnit * 2.5, + }); + + if (typeof page === "string") { + // url => load component and then load item from component + const component = Qt.createComponent(Qt.resolvedUrl(page)); + item = component.createObject(dialog.contentItem, properties); + component.destroy(); + dialog.contentItem.contentItem = item + } else if (page instanceof Component) { + item = page.createObject(dialog.contentItem, properties); + dialog.contentItem.contentItem = item + } else if (page instanceof Item) { + item = page; + page.parent = dialog.contentItem; + } else if (typeof page === 'object' && typeof page.toString() === 'string') { // url + const component = Qt.createComponent(page); + item = component.createObject(dialog.contentItem, properties); + component.destroy(); + dialog.contentItem.contentItem = item + } + dialog.title = Qt.binding(() => item.title); + + // Pushing a PageRow is supported but without PageRow toolbar + if (item.globalToolBar && item.globalToolBar.style) { + item.globalToolBar.style = Kirigami.ApplicationHeaderStyle.None + } + Object.defineProperty(item, 'closeDialog', { + value: function() { + dialog.close(); + } + }); + dialog.open(); + } else { + // open as a layer + properties.globalToolBarStyle = root.globalToolBar.style + item = layers.push(page, properties); + Object.defineProperty(item, 'closeDialog', { + value: function() { + layers.pop(); + } + }); + } + } else { + // open as a new window + if (!("modality" in windowProperties)) { + windowProperties.modality = Qt.WindowModal; + } + if (!("height" in windowProperties)) { + windowProperties.height = Kirigami.Units.gridUnit * 30; + } + if (!("width" in windowProperties)) { + windowProperties.width = Kirigami.Units.gridUnit * 50; + } + if (!("minimumWidth" in windowProperties)) { + windowProperties.minimumWidth = Kirigami.Units.gridUnit * 20; + } + if (!("minimumHeight" in windowProperties)) { + windowProperties.minimumHeight = Kirigami.Units.gridUnit * 15; + } + if (!("flags" in windowProperties)) { + windowProperties.flags = Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint; + } + const windowComponent = Qt.createComponent(Qt.resolvedUrl("./ApplicationWindow.qml")); + const window = windowComponent.createObject(root, windowProperties); + windowComponent.destroy(); + item = window.pageStack.push(page, properties); + Object.defineProperty(item, 'closeDialog', { + value: function() { + window.close(); + } + }); + } + item.Keys.escapePressed.connect(event => item.closeDialog()); + return item; + } + + /** + * @brief Inserts a new page or a list of new pages at an arbitrary position. + * + * A single page can be defined as an url, a component, or an object. It can + * also be an array of the above said types, but in that case, the + * properties' array length must match pages' array length or it must be + * empty. Failing to comply with the following rules will make the method + * return null before doing anything. + * + * @param page A single page or an array of pages. + * @param properties A single property object or an array of property + * objects. + * + * @return The new created page (or the last one if it was an array). + * @since 2.7 + */ + function insertPage(position, page, properties): QT.Page { + if (!pagesLogic.verifyPages(page, properties)) { + console.warn("Inserted pages do not conform to the rules. Please check the documentation."); + console.trace(); + return null + } + + if (position < 0 || position > depth) { + console.warn("You are trying to insert a page to an out-of-bounds position. Position will be adjusted accordingly."); + console.trace(); + position = Math.max(0, Math.min(depth, position)); + } + return pagesLogic.insertPage_unchecked(position, page, properties) + } + + /** + * Move the page at position fromPos to the new position toPos + * If needed, currentIndex will be adjusted + * in order to keep the same current page. + * @since 2.7 + */ + function movePage(fromPos, toPos): void { + columnView.moveItem(fromPos, toPos); + } + + /** + * @brief Remove the given page. + * @param page The page can be given both as integer position or by reference + * @return The page that has just been removed + * @since 2.7 + */ + function removePage(page): QT.Page { + if (depth > 0) { + return columnView.removeItem(page); + } + return null + } + + /** + * @brief Pops a page off the stack. + * @param page If page is specified then the stack is unwound to that page, + * to unwind to the first page specify page as null. + * @return The page instance that was popped off the stack. + */ + function pop(page): QT.Page { + return columnView.pop(page); + } + + /** + * @brief Replaces a page on the current index. + * + * A single page can be defined as an url, a component, or an object. It can + * also be an array of the above said types, but in that case, the + * properties' array length must match pages' array length or it must be + * empty. Failing to comply with the following rules will make the method + * return null before doing anything. + * + * @param page A single page or an array of pages. + * @param properties A single property object or an array of property + * objects. + * + * @return The new created page (or the last one if it was an array). + * @see push() for details. + */ + function replace(page, properties): QT.Page { + if (!pagesLogic.verifyPages(page, properties)) { + console.warn("Specified pages do not conform to the rules. Please check the documentation."); + console.trace(); + return null + } + + // Remove all pages on top of the one being replaced. + if (currentIndex >= 0) { + columnView.pop(currentIndex); + } else { + console.warn("There's no page to replace"); + } + + // Figure out if more than one page is being pushed. + let pages; + let propsArray = []; + if (page instanceof Array) { + pages = page; + page = pages.shift(); + } + if (properties instanceof Array) { + propsArray = properties; + properties = propsArray.shift(); + } else { + propsArray = [properties]; + } + + // Replace topmost page. + let pageItem = pagesLogic.initPage(page, properties); + if (depth > 0) + columnView.replaceItem(depth - 1, pageItem); + else { + console.log("Calling replace on empty PageRow", pageItem) + columnView.addItem(pageItem) + } + pagePushed(pageItem); + + // Push any extra defined pages onto the stack. + if (pages) { + for (const i in pages) { + const tPage = pages[i]; + const tProps = propsArray[i]; + + pageItem = pagesLogic.initPage(tPage, tProps); + columnView.addItem(pageItem); + pagePushed(pageItem); + } + } + + currentIndex = depth - 1; + return pageItem; + } + + /** + * @brief Clears the page stack. + * + * Destroy (or reparent) all the pages contained. + */ + function clear(): void { + columnView.clear(); + } + + /** + * @return the page at idx + * @param idx the depth of the page we want + */ + function get(idx): QT.Page { + return items[idx]; + } + + /** + * Go back to the previous index and scroll to the left to show one more column. + */ + function flickBack(): void { + if (depth > 1) { + currentIndex = Math.max(0, currentIndex - 1); + } + } + + /** + * Acts as if you had pressed the "back" button on Android or did Alt-Left on desktop, + * "going back" in the layers and page row. Results in a layer being popped if available, + * or the currentIndex being set to currentIndex-1 if not available. + * + * @param event Optional, an event that will be accepted if a page is successfully + * "backed" on + */ + function goBack(event = null): void { + const backEvent = {accepted: false} + + if (layersStack.depth >= 1) { + try { // app code might be screwy, but we still want to continue functioning if it throws an exception + layersStack.currentItem.backRequested(backEvent) + } catch (error) {} + + if (!backEvent.accepted) { + if (layersStack.depth > 1) { + layersStack.pop() + if (event) { + event.accepted = true + } + return + } + } + } + + if (currentIndex >= 1) { + try { // app code might be screwy, but we still want to continue functioning if it throws an exception + currentItem.backRequested(backEvent) + } catch (error) {} + + if (!backEvent.accepted) { + if (depth > 1) { + currentIndex = Math.max(0, currentIndex - 1) + if (event) { + event.accepted = true + } + } + } + } + } + + /** + * Acts as if you had pressed the "forward" shortcut on desktop, + * "going forward" in the page row. Results in the active page + * becoming the next page in the row from the current active page, + * i.e. currentIndex + 1. + */ + function goForward(): void { + currentIndex = Math.min(depth - 1, currentIndex + 1) + } +//END FUNCTIONS + +//BEGIN signals & signal handlers + /** + * @brief Emitted when a page has been inserted anywhere. + * @param position where the page has been inserted + * @param page the new page + * @since 2.7 + */ + signal pageInserted(int position, Item page) + + /** + * @brief Emitted when a page has been pushed to the bottom. + * @param page the new page + * @since 2.5 + */ + signal pagePushed(Item page) + + /** + * @brief Emitted when a page has been removed from the row. + * @param page the page that has been removed: at this point it's still valid, + * but may be auto deleted soon. + * @since 2.5 + */ + signal pageRemoved(Item page) + + onLeftSidebarChanged: { + if (leftSidebar && !leftSidebar.modal) { + modalConnection.onModalChanged(); + } + } + + Keys.onReleased: event => { + if (event.key === Qt.Key_Back) { + this.goBack(event) + } + } + + onInitialPageChanged: { + if (initialPage) { + clear(); + push(initialPage, null) + } + } +/* + onActiveFocusChanged: { + if (activeFocus) { + layersStack.currentItem.forceActiveFocus() + if (columnView.activeFocus) { + print("SSS"+columnView.currentItem) + columnView.currentItem.forceActiveFocus(); + } + } + } +*/ +//END signals & signal handlers + + Connections { + id: modalConnection + target: leftSidebar + function onModalChanged(): void { + if (leftSidebar.modal) { + const sidebar = sidebarControl.contentItem; + const background = sidebarControl.background; + sidebarControl.contentItem = null; + leftSidebar.contentItem = sidebar; + sidebarControl.background = null; + leftSidebar.background = background; + + sidebar.visible = true; + background.visible = true; + } else { + const sidebar = leftSidebar.contentItem + const background = leftSidebar.background + leftSidebar.contentItem=null + sidebarControl.contentItem = sidebar + leftSidebar.background=null + sidebarControl.background = background + + sidebar.visible = true; + background.visible = true; + } + } + } + + implicitWidth: contentItem.implicitWidth + leftPadding + rightPadding + implicitHeight: contentItem.implicitHeight + topPadding + bottomPadding + + Shortcut { + sequences: [ StandardKey.Back ] + onActivated: goBack() + } + Shortcut { + sequences: [ StandardKey.Forward ] + onActivated: goForward() + } + + Keys.forwardTo: [currentItem] + + GlobalToolBar.PageRowGlobalToolBarStyleGroup { + id: globalToolBar + readonly property int leftReservedSpace: globalToolBarUI.item ? globalToolBarUI.item.leftReservedSpace : 0 + readonly property int rightReservedSpace: globalToolBarUI.item ? globalToolBarUI.item.rightReservedSpace : 0 + readonly property int height: globalToolBarUI.height + readonly property Item leftHandleAnchor: globalToolBarUI.item ? globalToolBarUI.item.leftHandleAnchor : null + readonly property Item rightHandleAnchor: globalToolBarUI.item ? globalToolBarUI.item.rightHandleAnchor : null + } + + QQC2.StackView { + id: layerToolbarStack + anchors { + left: parent.left + top: parent.top + right: parent.right + } + z: 100 // 100 is layersStack.z + 1 + height: currentItem?.implicitHeight ?? 0 + initialItem: Item {implicitHeight: 0} + + Component { + id: emptyToolbar + Item { + implicitHeight: 0 + } + } + popEnter: Transition { + OpacityAnimator { + from: 0 + to: 1 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + } + popExit: Transition { + OpacityAnimator { + from: 1 + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + } + pushEnter: Transition { + OpacityAnimator { + from: 0 + to: 1 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + } + pushExit: Transition { + OpacityAnimator { + from: 1 + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + } + replaceEnter: Transition { + OpacityAnimator { + from: 0 + to: 1 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + } + replaceExit: Transition { + OpacityAnimator { + from: 1 + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + } + } + + QQC2.StackView { + id: layerFooterStack + anchors { + left: parent.left + bottom: parent.bottom + right: parent.right + } + z: 100 // 100 is layersStack.z + 1 + height: currentItem?.implicitHeight ?? 0 + initialItem: Item {implicitHeight: 0} + + popEnter: Transition { + OpacityAnimator { + from: 0 + to: 1 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + } + popExit: Transition { + OpacityAnimator { + from: 1 + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + } + pushEnter: Transition { + OpacityAnimator { + from: 0 + to: 1 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + } + pushExit: Transition { + OpacityAnimator { + from: 1 + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + } + replaceEnter: Transition { + OpacityAnimator { + from: 0 + to: 1 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + } + replaceExit: Transition { + OpacityAnimator { + from: 1 + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + } + } + + QQC2.StackView { + id: layersStack + z: 99 + anchors { + left: parent.left + top: layerToolbarStack.bottom + right: parent.right + bottom: layerFooterStack.top + } + // placeholder as initial item + initialItem: columnViewLayout + + onDepthChanged: { + let item = layersStack.get(depth - 1) + + if (layerToolbarStack.depth > depth) { + while (layerToolbarStack.depth > depth) { + layerToolbarStack.pop(); + } + } else if (layerToolbarStack.depth < depth) { + for (let i = layerToolbarStack.depth; i < depth; ++i) { + const toolBar = layersStack.get(i).Kirigami.ColumnView.globalHeader; + layerToolbarStack.push(toolBar || emptyToolbar); + } + } + let toolBarItem = layerToolbarStack.get(layerToolbarStack.depth - 1) + if (item.Kirigami.ColumnView.globalHeader != toolBarItem) { + const toolBar = item.Kirigami.ColumnView.globalHeader; + layerToolbarStack.replace(toolBar ?? emptyToolbar); + } + // WORKAROUND: the second time the transition on opacity doesn't seem to be executed + toolBarItem = layerToolbarStack.get(layerToolbarStack.depth - 1) + toolBarItem.opacity = 1; + + if (layerFooterStack.depth > depth) { + while (layerFooterStack.depth > depth) { + layerFooterStack.pop(); + } + } else if (layerFooterStack.depth < depth) { + for (let i = layerFooterStack.depth; i < depth; ++i) { + const footer = layersStack.get(i).Kirigami.ColumnView.globalFooter; + layerFooterStack.push(footer ?? emptyToolbar); + } + } + let footerItem = layerFooterStack.get(layerFooterStack.depth - 1) + if (item.Kirigami.ColumnView.globalHeader != footerItem) { + const footer = item.Kirigami.ColumnView.globalFooter; + layerFooterStack.replace(footer ?? emptyToolbar); + } + footerItem = layerFooterStack.get(layerFooterStack.depth - 1) + footerItem.opacity = 1; + } + + function clear(): void { + // don't let it kill the main page row + const d = layersStack.depth; + for (let i = 1; i < d; ++i) { + pop(); + } + } + + popEnter: Transition { + OpacityAnimator { + from: 0 + to: 1 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + } + popExit: Transition { + ParallelAnimation { + OpacityAnimator { + from: 1 + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + YAnimator { + from: 0 + to: height/2 + duration: Kirigami.Units.longDuration + easing.type: Easing.InCubic + } + } + } + + pushEnter: Transition { + ParallelAnimation { + // NOTE: It's a PropertyAnimation instead of an Animator because with an animator the item will be visible for an instant before starting to fade + PropertyAnimation { + property: "opacity" + from: 0 + to: 1 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + YAnimator { + from: height/2 + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.OutCubic + } + } + } + + + pushExit: Transition { + OpacityAnimator { + from: 1 + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + } + + replaceEnter: Transition { + ParallelAnimation { + OpacityAnimator { + from: 0 + to: 1 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + YAnimator { + from: height/2 + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.OutCubic + } + } + } + + replaceExit: Transition { + ParallelAnimation { + OpacityAnimator { + from: 1 + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.InCubic + } + YAnimator { + from: 0 + to: -height/2 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + } + } + } + + Loader { + id: globalToolBarUI + anchors { + left: parent.left + top: parent.top + right: parent.right + } + z: 100 + property QT.Control pageRow: root + active: globalToolBar.actualStyle !== Kirigami.ApplicationHeaderStyle.None || (leadingVisibleItem && leadingVisibleItem.globalToolBarStyle === Kirigami.ApplicationHeaderStyle.ToolBar) + visible: active + height: active ? implicitHeight : 0 + // If load is asynchronous, it will fail to compute the initial implicitHeight + // https://bugs.kde.org/show_bug.cgi?id=442660 + asynchronous: false + source: Qt.resolvedUrl("private/globaltoolbar/PageRowGlobalToolBarUI.qml"); + } + + QtObject { + id: pagesLogic + readonly property var componentCache: new Array() + + property Component __mobileDialogLayerComponent + + function getMobileDialogLayerComponent() { + if (!__mobileDialogLayerComponent) { + __mobileDialogLayerComponent = Qt.createComponent(Qt.resolvedUrl("private/MobileDialogLayer.qml")); + } + return __mobileDialogLayerComponent; + } + + function verifyPages(pages, properties): bool { + function validPage(page) { + //don't try adding an already existing page + if (page instanceof QT.Page && columnView.containsItem(page)) { + console.log(`Page ${page} is already in the PageRow`) + return false + } + return page instanceof QT.Page || page instanceof Component || typeof page === 'string' + || (typeof page === 'object' && typeof page.toString() === 'string') + } + + // check page/pages that it is/they are valid + const pagesIsArr = Array.isArray(pages) && pages.length > 0 + let isValidArrOfPages = pagesIsArr; + + if (pagesIsArr) { + for (const page of pages) { + if (!validPage(page)) { + isValidArrOfPages = false; + break; + } + } + } + + // check properties obejct/array object validity + const isProp = typeof properties === 'object'; + const propsIsArr = Array.isArray(properties) && properties.length > 0 + let isValidPropArr = propsIsArr; + + if (propsIsArr) { + for (const prop of properties) { + if (typeof prop !== 'object') { + isValidPropArr = false; + break; + } + } + isValidPropArr = isValidPropArr && pages.length === properties.length + } + + return (validPage(pages) || isValidArrOfPages) + && (!properties || (isProp || isValidPropArr)) + } + + function insertPage_unchecked(position, page, properties) { + columnView.pop(position - 1); + + // figure out if more than one page is being pushed + let pages; + let propsArray = []; + if (page instanceof Array) { + pages = page; + page = pages.pop(); + } + if (properties instanceof Array) { + propsArray = properties; + properties = propsArray.pop(); + } else { + propsArray = [properties]; + } + + // push any extra defined pages onto the stack + if (pages) { + for (const i in pages) { + let tPage = pages[i]; + let tProps = propsArray[i]; + + pagesLogic.initAndInsertPage(position, tPage, tProps); + ++position; + } + } + + // initialize the page + const pageItem = pagesLogic.initAndInsertPage(position, page, properties); + + pagePushed(pageItem); + + return pageItem; + } + + function getPageComponent(page): Component { + let pageComp; + + if (page.createObject) { + // page defined as component + pageComp = page; + } else if (typeof page === "string") { + // page defined as string (a url) + pageComp = pagesLogic.componentCache[page]; + if (!pageComp) { + pageComp = pagesLogic.componentCache[page] = Qt.createComponent(page); + } + } else if (typeof page === "object" && !(page instanceof Item) && page.toString !== undefined) { + // page defined as url (QML value type, not a string) + pageComp = pagesLogic.componentCache[page.toString()]; + if (!pageComp) { + pageComp = pagesLogic.componentCache[page.toString()] = Qt.createComponent(page.toString()); + } + } + + return pageComp + } + + function initPage(page, properties): QT.Page { + const pageComp = getPageComponent(page, properties); + + if (pageComp) { + // instantiate page from component + // Important: The parent needs to be set otherwise a reference needs to be kept around + // to avoid the page being garbage collected. + page = pageComp.createObject(pagesLogic, properties || {}); + + if (pageComp.status === Component.Error) { + throw new Error("Error while loading page: " + pageComp.errorString()); + } + } else { + // copy properties to the page + for (const prop in properties) { + if (page.hasOwnProperty(prop)) { + page[prop] = properties[prop]; + } + } + } + return page; + } + + function initAndInsertPage(position, page, properties): QT.Page { + page = initPage(page, properties); + columnView.insertItem(position, page); + return page; + } + } + + RowLayout { + id: columnViewLayout + spacing: 1 + readonly property alias columnView: columnView + // set the pagestack of this and all children to root, otherwise + // they would automatically resolve to the layer's stackview + Kirigami.PageStack.pageStack: root + anchors { + fill: parent + topMargin: -layersStack.y + } + QQC2.Control { + id: sidebarControl + Layout.fillHeight: true + visible: contentItem !== null + leftPadding: root.leftSidebar ? root.leftSidebar.leftPadding : 0 + topPadding: root.leftSidebar ? root.leftSidebar.topPadding : 0 + rightPadding: root.leftSidebar ? root.leftSidebar.rightPadding : 0 + bottomPadding: root.leftSidebar ? root.leftSidebar.bottomPadding : 0 + } + Kirigami.ColumnView { + id: columnView + Layout.fillWidth: true + Layout.fillHeight: true + + topPadding: globalToolBarUI.item && globalToolBarUI.item.breadcrumbVisible + ? globalToolBarUI.height : 0 + + // Internal hidden api for Page + readonly property Item __pageRow: root + acceptsMouse: Kirigami.Settings.isMobile + columnResizeMode: root.wideMode ? Kirigami.ColumnView.FixedColumns : Kirigami.ColumnView.SingleColumn + columnWidth: root.defaultColumnWidth + interactive: Qt.platform.os !== 'android' + + onItemInserted: (position, item) => root.pageInserted(position, item); + onItemRemoved: item => root.pageRemoved(item); + + onVisibleItemsChanged: { + // implementation of `popHiddenPages` option + if (root.popHiddenPages) { + // manually fetch lastItem here rather than use root.lastItem property, since that binding may not have updated yet + let lastItem = columnView.contentChildren[columnView.contentChildren.length - 1]; + let trailingVisibleItem = columnView.trailingVisibleItem; + + // pop every page that isn't visible and at the top of the stack + while (lastItem && columnView.trailingVisibleItem && + lastItem !== columnView.trailingVisibleItem && columnView.containsItem(lastItem)) { + root.pop(); + } + } + } + } + } + + Rectangle { + anchors.bottom: parent.bottom + height: Kirigami.Units.smallSpacing + x: (columnView.width - width) * (columnView.contentX / (columnView.contentWidth - columnView.width)) + width: columnView.width * (columnView.width/columnView.contentWidth) + color: Kirigami.Theme.textColor + opacity: 0 + onXChanged: { + opacity = 0.3 + scrollIndicatorTimer.restart(); + } + Behavior on opacity { + OpacityAnimator { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + Timer { + id: scrollIndicatorTimer + interval: Kirigami.Units.longDuration * 4 + onTriggered: parent.opacity = 0; + } + } +} diff --git a/src/controls/PasswordField.qml b/src/controls/PasswordField.qml new file mode 100644 index 0000000..bc3e837 --- /dev/null +++ b/src/controls/PasswordField.qml @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2019 Carl-Lucien Schwan + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +/** + * @brief This is a standard password text field. + * + * Example usage: + * @code{.qml} + * import org.kde.kirigami as Kirigami + * + * Kirigami.PasswordField { + * id: passwordField + * onAccepted: { + * // check if passwordField.text is valid + * } + * } + * @endcode + * + * @inherit org::kde::kirgami::ActionTextField + * @since 5.57 + * @author Carl Schwan + */ +Kirigami.ActionTextField { + id: root + + /** + * @brief This property tells whether the password will be displayed in cleartext rather than obfuscated. + * + * default: ``false`` + * + * @since 5.57 + */ + property bool showPassword: false + + echoMode: root.showPassword ? TextInput.Normal : TextInput.Password + placeholderText: qsTr("Password") + inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText | Qt.ImhSensitiveData + rightActions: Kirigami.Action { + text: root.showPassword ? qsTr("Hide Password") : qsTr("Show Password") + icon.name: root.showPassword ? "password-show-off" : "password-show-on" + onTriggered: root.showPassword = !root.showPassword + } + + Keys.onPressed: event => { + if (event.matches(StandardKey.Undo)) { + // Disable undo action for security reasons + // See QTBUG-103934 + event.accepted = true + } + } +} diff --git a/src/controls/PlaceholderMessage.qml b/src/controls/PlaceholderMessage.qml new file mode 100644 index 0000000..eac747b --- /dev/null +++ b/src/controls/PlaceholderMessage.qml @@ -0,0 +1,298 @@ +/* + * SPDX-FileCopyrightText: 2020 Nate Graham + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import "private" as P + +/** + * @brief A placeholder message indicating that a view is empty. + * + * The message comprises a label with text, an optional explanation below the main text, + * an optional icon above all the text, and an optional button below all the text which + * can be used to easily show the user what to do next to add content to the view. + * + * The explanatory text is selectable and can contain clickable links. In this latter + * case, client code must implement an ``onLinkactivated:`` signal handler or the links + * will not work. + * + * The top-level component is a ColumnLayout, so additional components items can + * simply be added as child items and they will be positioned sanely. + * + * Example usage: + * @code{.qml} + ** used as a "this view is empty" message + * import org.kde.kirigami as Kirigami + * + * ListView { + * id: listView + * model: [...] + * delegate: [...] + * + * Kirigami.PlaceholderMessage { + * anchors.centerIn: parent + * width: parent.width - (Kirigami.Units.largeSpacing * 4) + * + * visible: listView.count === 0 + * + * text: "There are no items in this list" + * } + * } + * @endcode + * @code{.qml} + ** Used as a "here's how to proceed" message + * import org.kde.kirigami as Kirigami + * + * ListView { + * id: listView + * model: [...] + * delegate: [...] + * + * Kirigami.PlaceholderMessage { + * anchors.centerIn: parent + * width: parent.width - (Kirigami.Units.largeSpacing * 4) + * + * visible: listView.count === 0 + * + * text: "Add an item to proceed" + * + * helpfulAction: Kirigami.Action { + * icon.name: "list-add" + * text: "Add item..." + * onTriggered: { + * [...] + * } + * } + * } + * [...] + * } + * @endcode + * @code{.qml} + ** Used as a "there was a problem here" message + * import org.kde.kirigami as Kirigami + * + * Kirigami.Page { + * id: root + * readonly property bool networkConnected: [...] + * + * Kirigami.PlaceholderMessage { + * anchors.centerIn: parent + * width: parent.width - (Kirigami.Units.largeSpacing * 4) + * + * visible: root.networkConnected + * + * icon.name: "network-disconnect" + * text: "Unable to load content" + * explanation: "Please try again later." + * " Visit Qt.openUrlExternally(link) + * } + * } + * @endcode + * @code{.qml} + * import org.kde.kirigami as Kirigami + * + ** Used as a "Here's what you do next" button + * Kirigami.Page { + * id: root + * + * Kirigami.PlaceholderMessage { + * anchors.centerIn: parent + * width: parent.width - (Kirigami.Units.largeSpacing * 4) + * + * visible: root.loading + * + * helpfulAction: Kirigami.Action { + * icon.name: "list-add" + * text: "Add item..." + * onTriggered: { + * [...] + * } + * } + * } + * } + * @endcode + * @inherit QtQuick.Layouts.ColumnLayout + * @since 2.12 + */ +ColumnLayout { + id: root + + enum Type { + Actionable, + Informational + } + +//BEGIN properties + /** + * @brief This property holds the PlaceholderMessage type. + * + * The type of the message. This can be: + * * ``Kirigami.PlaceholderMessage.Type.Actionable``: Makes it more attention-getting. Useful when the user is expected to interact with the message. + * * ``Kirigami.PlaceholderMessage.Type.Informational``: Makes it less prominent. Useful when the message in only informational. + * + * default: `if a helpfulAction is provided this will be of type Actionable otherwise of type Informational.` + * + * @since 5.94 + */ + property int type: actionButton.action?.enabled + ? PlaceholderMessage.Type.Actionable + : PlaceholderMessage.Type.Informational + + /** + * @brief This property holds the text to show in the placeholder label. + * + * Optional; if not defined, the message will have no large text label + * text. If both text: and explanation: are omitted, the message will have + * no text and only an icon, action button, and/or other custom content. + * + * @since 5.70 + */ + property string text + + /** + * @brief This property holds the smaller explanatory text to show below the larger title-style text + * + * Useful for providing a user-friendly explanation on how to proceed. + * + * Optional; if not defined, the message will have no supplementary + * explanatory text. + * + * @since 5.80 + */ + property string explanation + + /** + * @brief This property provides an icon to display above the top text label. + * @note It accepts ``icon.name`` and ``icon.source`` to set the icon source. + * It is suggested to use ``icon.name``. + * + * Optional; if undefined, the message will have no icon. + * Falls back to `undefined` if the specified icon is not valid or cannot + * be loaded. + * + * @see org::kde::kirigami::private::ActionIconGroup + * @since 5.70 + */ + readonly property P.ActionIconGroup icon: P.ActionIconGroup { + width: Math.round(Kirigami.Units.iconSizes.huge * 1.5) + height: Math.round(Kirigami.Units.iconSizes.huge * 1.5) + color: Kirigami.Theme.textColor + } + + /** + * @brief This property holds an action that helps the user proceed. + * + * Typically used to guide the user to the next step for adding + * content or items to an empty view. + * + * Optional; if undefined, no button will appear below the text label. + * + * @property QtQuick.Controls.Action helpfulAction + * @since 5.70 + */ + property alias helpfulAction: actionButton.action + + /** + * This property holds the link embedded in the explanatory message text that + * the user is hovering over. + */ + readonly property alias hoveredLink: label.hoveredLink + + /** + * This signal is emitted when a link is hovered in the explanatory message + * text. + * @param The hovered link. + */ + signal linkHovered(string link) + + /** + * This signal is emitted when a link is clicked or tapped in the explanatory + * message text. + * @param The clicked or tapped link. + */ + signal linkActivated(string link) +//END properties + + spacing: Kirigami.Units.largeSpacing + + Component.onCompleted: _announce(); + onVisibleChanged: { + _announce(); + } + function _announce() + { + if (visible && Accessible.announce) { + // Accessible.announce was added in Qt 6.8.0 + if (root.text.length > 0) + Accessible.announce(root.text); + if (root.explanation.length > 0) + Accessible.announce(root.explanation); + } + } + + Kirigami.Icon { + visible: source !== undefined + opacity: root.type === PlaceholderMessage.Type.Actionable ? 1 : 0.5 + + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: root.icon.width + Layout.preferredHeight: root.icon.height + + color: root.icon.color + + source: { + if (root.icon.source.length > 0) { + return root.icon.source + } else if (root.icon.name.length > 0) { + return root.icon.name + } + return undefined + } + } + + Kirigami.Heading { + text: root.text + visible: text.length > 0 + + type: Kirigami.Heading.Primary + opacity: root.type === PlaceholderMessage.Type.Actionable ? 1 : 0.65 + + + Layout.fillWidth: true + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + + wrapMode: Text.WordWrap + } + + Kirigami.SelectableLabel { + id: label + + text: root.explanation + visible: root.explanation.length > 0 + opacity: root.type === PlaceholderMessage.Type.Actionable ? 1 : 0.65 + + horizontalAlignment: Qt.AlignHCenter + wrapMode: Text.WordWrap + + Layout.fillWidth: true + + onLinkHovered: link => root.linkHovered(link) + onLinkActivated: link => root.linkActivated(link) + } + + QQC2.Button { + id: actionButton + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Kirigami.Units.gridUnit + + visible: action?.enabled ?? false + } +} diff --git a/src/controls/ScrollablePage.qml b/src/controls/ScrollablePage.qml new file mode 100644 index 0000000..13fc148 --- /dev/null +++ b/src/controls/ScrollablePage.qml @@ -0,0 +1,361 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQml +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import org.kde.kirigami.templates as KT +import "private" + + +// TODO KF6: undo many workarounds to make existing code work? + +/** + * @brief ScrollablePage is a Page that holds scrollable content, such as a ListView. + * + * Scrolling and scrolling indicators will be automatically managed. + * + * Example usage: + * @code + * ScrollablePage { + * id: root + * // The page will automatically be scrollable + * Rectangle { + * width: root.width + * height: 99999 + * } + * } + * @endcode + * + * @warning Do not put a ScrollView inside of a ScrollablePage; children of a ScrollablePage are already inside a ScrollView. + * + * Another behavior added by this class is a "scroll down to refresh" behavior + * It also can give the contents of the flickable to have more top margins in order + * to make possible to scroll down the list to reach it with the thumb while using the + * phone with a single hand. + * + * Implementations should handle the refresh themselves as follows + * + * Example usage: + * @code + * Kirigami.ScrollablePage { + * id: view + * supportsRefreshing: true + * onRefreshingChanged: { + * if (refreshing) { + * myModel.refresh(); + * } + * } + * ListView { + * // NOTE: MyModel doesn't come from the components, + * // it's purely an example on how it can be used together + * // some application logic that can update the list model + * // and signals when it's done. + * model: MyModel { + * onRefreshDone: view.refreshing = false; + * } + * delegate: ItemDelegate {} + * } + * } + * [...] + * @endcode + */ +Kirigami.Page { + id: root + +//BEGIN properties + /** + * @brief This property tells whether the list is asking for a refresh. + * + * This property will automatically be set to true when the user pulls the list down enough, + * which in return, shows a loading spinner. When this is set to true, it signals + * the application logic to start its refresh procedure. + * + * default: ``false`` + * + * @note The application itself will have to set back this property to false when done. + */ + property bool refreshing: false + + /** + * @brief This property sets whether scrollable page supports "pull down to refresh" behaviour. + * + * default: ``false`` + */ + property bool supportsRefreshing: false + + /** + * @brief This property holds the main Flickable item of this page. + * @deprecated here for compatibility; will be removed in KF6. + */ + property Flickable flickable: Flickable {} // FIXME KF6: this empty flickable exists for compatibility reasons. some apps assume flickable exists right from the beginning but ScrollView internally assumes it does not + onFlickableChanged: scrollView.contentItem = flickable; + + /** + * @brief This property sets the vertical scrollbar policy. + * @property Qt::ScrollBarPolicy verticalScrollBarPolicy + */ + property int verticalScrollBarPolicy + + /** + * @brief Set if the vertical scrollbar should be interactable. + * @property bool verticalScrollBarInteractive + */ + property bool verticalScrollBarInteractive: true + + /** + * @brief This property sets the horizontal scrollbar policy. + * @property Qt::ScrollBarPolicy horizontalScrollBarPolicy + */ + property int horizontalScrollBarPolicy: QQC2.ScrollBar.AlwaysOff + + /** + * @brief Set if the horizontal scrollbar should be interactable. + * @property bool horizontalScrollBarInteractive + */ + property bool horizontalScrollBarInteractive: true + + default property alias scrollablePageData: itemsParent.data + property alias scrollablePageChildren: itemsParent.children + + /* + * @deprecated here for compatibility; will be removed in KF6. + */ + property QtObject mainItem + onMainItemChanged: { + print("Warning: the mainItem property is deprecated"); + scrollablePageData.push(mainItem); + } + + /** + * @brief This property sets whether it is possible to navigate the items in a view that support it. + * + * If true, and if flickable is an item view (e.g. ListView, GridView), it will be possible + * to navigate the view current items with keyboard up/down arrow buttons. + * Also, any key event will be forwarded to the current list item. + * + * default: ``true`` + */ + property bool keyboardNavigationEnabled: true +//END properties + +//BEGIN FUNCTIONS +/** + * @brief This method checks whether a particular child item is in view, and scrolls + * the page to center the item if it is not. + * + * If the page is a View, the view should handle this by itself, but if the page is a + * manually handled layout, this needs to be done manually. Otherwise, if the user + * passes focus to an item with e.g. keyboard navigation, this may be outside the + * visible area. + * + * When called, this method will place the visible area such that the item at the + * center if any part of it is currently outside. If the item is larger than the viewable + * area in either direction, the area will be scrolled such that the top left corner + * is visible. + * + * @code + * Kirigami.ScrollablePage { + * id: page + * ColumnLayout { + * Repeater { + * model: 100 + * delegate: QQC2.Button { + * text: modelData + * onFocusChanged: if (focus) page.ensureVisible(this) + * } + * } + * } + * } + * @endcode + * + * @param item The item that should be in the visible area of the flickable. Item coordinates need to be in the flickable's coordinate system. + * @param xOffset,yOffset (optional) Offsets to align the item's and the flickable's coordinate system< + */ + function ensureVisible(item: Item, xOffset: int, yOffset: int) { + var actualItemX = item.x + (xOffset ?? 0) + var actualItemY = item.y + (yOffset ?? 0) + var viewXPosition = (item.width <= root.flickable.width) + ? Math.round(actualItemX + item.width / 2 - root.flickable.width / 2) + : actualItemX + var viewYPosition = (item.height <= root.flickable.height) + ? Math.round(actualItemY + item.height / 2 - root.flickable.height / 2) + : actualItemY + if (actualItemX < root.flickable.contentX) { + root.flickable.contentX = Math.max(0, viewXPosition) + } else if ((actualItemX + item.width) > (root.flickable.contentX + root.flickable.width)) { + root.flickable.contentX = Math.min(root.flickable.contentWidth - root.flickable.width, viewXPosition) + } + if (actualItemY < root.flickable.contentY) { + root.flickable.contentY = Math.max(0, viewYPosition) + } else if ((actualItemY + item.height) > (root.flickable.contentY + root.flickable.height)) { + root.flickable.contentY = Math.min(root.flickable.contentHeight - root.flickable.height, viewYPosition) + } + root.flickable.returnToBounds() + } +//END FUNCTIONS + + implicitWidth: flickable?.contentItem?.implicitWidth + ?? Math.max(implicitBackgroundWidth + leftInset + rightInset, + contentWidth + leftPadding + rightPadding, + implicitHeaderWidth, + implicitFooterWidth) + + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + contentHeight + topPadding + bottomPadding + + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0) + + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0)) + + contentHeight: flickable?.contentHeight ?? 0 + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: flickable?.hasOwnProperty("model") ? Kirigami.Theme.View : Kirigami.Theme.Window + + Keys.forwardTo: { + if (root.keyboardNavigationEnabled && root.flickable) { + if (("currentItem" in root.flickable) && root.flickable.currentItem) { + return [ root.flickable.currentItem, root.flickable ]; + } else { + return [ root.flickable ]; + } + } else { + return []; + } + } + + contentItem: QQC2.ScrollView { + id: scrollView + anchors { + top: root.header?.visible + ? root.header.bottom + : parent.top + bottom: root.footer?.visible ? root.footer.top : parent.bottom + left: parent.left + right: parent.right + } + clip: true + QQC2.ScrollBar.horizontal.policy: root.horizontalScrollBarPolicy + QQC2.ScrollBar.horizontal.interactive: root.horizontalScrollBarInteractive + QQC2.ScrollBar.vertical.policy: root.verticalScrollBarPolicy + QQC2.ScrollBar.vertical.interactive: root.verticalScrollBarInteractive + } + + data: [ + // Has to be a MouseArea that accepts events otherwise touch events on Wayland will get lost + MouseArea { + id: scrollingArea + width: root.horizontalScrollBarPolicy === QQC2.ScrollBar.AlwaysOff ? root.flickable.width : Math.max(root.flickable.width, implicitWidth) + height: Math.max(root.flickable.height, implicitHeight) + implicitWidth: { + let implicit = 0; + for (const child of itemsParent.visibleChildren) { + if (child.implicitWidth > 0) { + implicit = Math.max(implicit, child.implicitWidth); + } + } + return implicit + itemsParent.anchors.leftMargin + itemsParent.anchors.rightMargin; + } + implicitHeight: { + let implicit = 0; + for (const child of itemsParent.visibleChildren) { + if (child.implicitHeight > 0) { + implicit = Math.max(implicit, child.implicitHeight); + } + } + return implicit + itemsParent.anchors.topMargin + itemsParent.anchors.bottomMargin; + } + Item { + id: itemsParent + property Flickable flickable + anchors { + fill: parent + topMargin: root.topPadding + leftMargin: root.leftPadding + rightMargin: root.rightPadding + bottomMargin: root.bottomPadding + } + onChildrenChanged: { + const child = children[children.length - 1]; + if (child instanceof QQC2.ScrollView) { + print("Warning: it's not supported to have ScrollViews inside a ScrollablePage") + } + } + } + Binding { + target: root.flickable + property: "bottomMargin" + value: root.bottomPadding + restoreMode: Binding.RestoreBinding + } + }, + + Loader { + id: busyIndicatorLoader + active: root.supportsRefreshing + sourceComponent: PullDownIndicator { + parent: root + active: root.refreshing + onTriggered: root.refreshing = true + } + } + ] + + Component.onCompleted: { + let flickableFound = false; + for (const child of itemsParent.data) { + if (child instanceof Flickable) { + // If there were more flickable children, take the last one, as behavior compatibility + // with old internal ScrollView + child.activeFocusOnTab = true; + root.flickable = child; + flickableFound = true; + if (child instanceof ListView) { + child.keyNavigationEnabled = true; + child.keyNavigationWraps = false; + } + } else if (child instanceof Item) { + child.anchors.left = itemsParent.left; + child.anchors.right = itemsParent.right; + } else if (child instanceof KT.OverlaySheet) { + // Reparent sheets, needs to be done before Component.onCompleted + if (child.parent === itemsParent || child.parent === null) { + child.parent = root; + } + } + } + + if (flickableFound) { + scrollView.contentItem = root.flickable; + root.flickable.parent = scrollView; + // The flickable needs focus only if the page didn't already explicitly set focus to some other control (eg a text field in the header) + Qt.callLater(() => { + if (root.activeFocus) { + root.flickable.forceActiveFocus(); + } + }); + // Some existing code incorrectly uses anchors + root.flickable.anchors.fill = undefined; + root.flickable.anchors.top = undefined; + root.flickable.anchors.left = undefined; + root.flickable.anchors.right = undefined; + root.flickable.anchors.bottom = undefined; + scrollingArea.visible = false; + } else { + scrollView.contentItem = root.flickable; + scrollingArea.parent = root.flickable.contentItem; + scrollingArea.visible = true; + root.flickable.contentHeight = Qt.binding(() => scrollingArea.implicitHeight - root.flickable.topMargin - root.flickable.bottomMargin); + root.flickable.contentWidth = Qt.binding(() => scrollingArea.implicitWidth); + scrollView.forceActiveFocus(Qt.TabFocusReason); // QTBUG-44043 : Focus on currentItem instead of pageStack itself + } + root.flickable.flickableDirection = Flickable.VerticalFlick; + + // HACK: Qt's default flick deceleration is too high, and we can't change it from plasma-integration, see QTBUG-121500 + root.flickable.flickDeceleration = 1500; + root.flickable.maximumFlickVelocity = 5000; + } +} diff --git a/src/controls/SearchField.qml b/src/controls/SearchField.qml new file mode 100644 index 0000000..1e4d2d2 --- /dev/null +++ b/src/controls/SearchField.qml @@ -0,0 +1,132 @@ +/* + * SPDX-FileCopyrightText: 2019 Carl-Lucien Schwan + * SPDX-FileCopyrightText: 2022 Felipe Kinoshita + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +/** + * @brief This is a standard TextField following the KDE HIG, which, by default, + * uses Ctrl+F as the focus keyboard shortcut and "Search…" as a placeholder text. + * + * Example usage for the search field component: + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.SearchField { + * id: searchField + * onAccepted: console.log("Search text is " + searchField.text) + * } + * @endcode + * + * @inherit org::kde::kirigami::ActionTextField + */ +Kirigami.ActionTextField { + id: root + /** + * @brief This property sets whether the accepted signal is fired automatically + * when the text is changed. + * + * Setting this to false will require that the user presses return or enter + * (the same way a QtQuick.Controls.TextInput works). + * + * default: ``true`` + * + * @since 5.81 + * @since org.kde.kirigami 2.16 + */ + property bool autoAccept: true + + /** + * @brief This property sets whether to delay automatic acceptance of the search input. + * + * Set this to true if your search is expensive (such as for online + * operations or in exceptionally slow data sets) and want to delay it + * for 2.5 seconds. + * + * @note If you must have immediate feedback (filter-style), use the + * text property directly instead of accepted() + * + * default: ``false`` + * + * @since 5.81 + * @since org.kde.kirigami 2.16 + */ + property bool delaySearch: false + + // padding to accommodate search icon nicely + leftPadding: if (effectiveHorizontalAlignment === TextInput.AlignRight) { + return _rightActionsRow.width + Kirigami.Units.smallSpacing + } else { + return searchIcon.width + Kirigami.Units.smallSpacing * 3 + } + rightPadding: if (effectiveHorizontalAlignment === TextInput.AlignRight) { + return searchIcon.width + Kirigami.Units.smallSpacing * 3 + } else { + return _rightActionsRow.width + Kirigami.Units.smallSpacing + } + + Kirigami.Icon { + id: searchIcon + LayoutMirroring.enabled: root.effectiveHorizontalAlignment === TextInput.AlignRight + anchors.left: root.left + anchors.leftMargin: Kirigami.Units.smallSpacing * 2 + anchors.verticalCenter: root.verticalCenter + anchors.verticalCenterOffset: Math.round((root.topPadding - root.bottomPadding) / 2) + implicitHeight: Kirigami.Units.iconSizes.sizeForLabels + implicitWidth: Kirigami.Units.iconSizes.sizeForLabels + color: root.placeholderTextColor + + source: "search" + } + + placeholderText: qsTr("Search…") + + Accessible.name: qsTr("Search") + Accessible.searchEdit: true + + focusSequence: StandardKey.Find + inputMethodHints: Qt.ImhNoPredictiveText + EnterKey.type: Qt.EnterKeySearch + rightActions: [ + Kirigami.Action { + //ltr confusingly refers to the direction of the arrow in the icon, not the text direction which it should be used in + icon.name: root.effectiveHorizontalAlignment === TextInput.AlignRight ? "edit-clear-locationbar-ltr" : "edit-clear-locationbar-rtl" + visible: root.text.length > 0 + text: qsTr("Clear search") + onTriggered: { + root.clear(); + // Since we are always sending the accepted signal here (whether or not the user has requested + // that the accepted signal be delayed), stop the delay timer that gets started by the text changing + // above, so that we don't end up sending two of those in rapid succession. + fireSearchDelay.stop(); + root.accepted(); + } + } + ] + + Timer { + id: fireSearchDelay + interval: root.delaySearch ? Kirigami.Units.humanMoment : Kirigami.Units.shortDuration + running: false + repeat: false + onTriggered: { + if (root.acceptableInput) { + root.accepted(); + } + } + } + onAccepted: { + fireSearchDelay.running = false + } + onTextChanged: { + if (root.autoAccept) { + fireSearchDelay.restart(); + } else { + fireSearchDelay.stop(); + } + } +} diff --git a/src/controls/SelectableLabel.qml b/src/controls/SelectableLabel.qml new file mode 100644 index 0000000..d8c9032 --- /dev/null +++ b/src/controls/SelectableLabel.qml @@ -0,0 +1,202 @@ +/* + * SPDX-FileCopyrightText: 2022 Fushan Wen + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * SPDX-FileCopyrightText: 2024 Akseli Lahtinen + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T + +/** + * @brief This is a label which supports text selection. + * + * The label uses TextEdit component, which is wrapped inside a Control component. + * + * + * Example usage: + * @code{.qml} + * Kirigami.SelectableLabel { + * text: "Label" + * } + * @endcode + * + * @see https://bugreports.qt.io/browse/QTBUG-14077 + * @since org.kde.kirigami 2.20 + * @since 5.95 + * @inherit QtQuick.Control + */ + +QQC2.Control { + id: root + + //TODO KF7: Cleanup from unnecessary properties we dont need to expose for a label + activeFocusOnTab: false + + padding: undefined + topPadding: undefined + leftPadding: undefined + rightPadding: undefined + bottomPadding: undefined + + Accessible.name: textEdit.text + Accessible.role: Accessible.StaticText + Accessible.selectableText: true + Accessible.editable: false + + property bool contextMenuEnabled: true + + property alias readOnly: textEdit.readOnly + property alias selectByMouse: textEdit.selectByMouse + property alias color: textEdit.color + property alias selectedTextColor: textEdit.selectedTextColor + property alias selectionColor: textEdit.selectionColor + property alias text: textEdit.text + property alias baseUrl: textEdit.baseUrl + property var cursorShape + property alias horizontalAlignment: textEdit.horizontalAlignment + property alias verticalAlignment: textEdit.verticalAlignment + property alias textFormat: textEdit.textFormat + property alias wrapMode: textEdit.wrapMode + + property alias activeFocusOnPress: textEdit.activeFocusOnPress + property alias cursorDelegate: textEdit.cursorDelegate + property alias cursorPosition: textEdit.cursorPosition + property alias cursorVisible: textEdit.cursorVisible + property alias inputMethodHints: textEdit.inputMethodHints + property alias mouseSelectionMode: textEdit.mouseSelectionMode + property alias overwriteMode: textEdit.overwriteMode + property alias persistentSelection: textEdit.persistentSelection + property alias renderType: textEdit.renderType + property alias selectByKeyboard: textEdit.selectByKeyboard + property alias tabStopDistance: textEdit.tabStopDistance + property alias textMargin: textEdit.textMargin + + readonly property alias canPaste: textEdit.canPaste + readonly property alias canRedo: textEdit.canRedo + readonly property alias canUndo: textEdit.canUndo + readonly property alias inputMethodComposing: textEdit.inputMethodComposing + readonly property alias length: textEdit.length + readonly property alias lineCount: textEdit.lineCount + readonly property alias selectionEnd: textEdit.selectionEnd + readonly property alias selectionStart: textEdit.selectionStart + readonly property alias contentHeight: textEdit.contentHeight + readonly property alias contentWidth: textEdit.contentWidth + readonly property alias hoveredLink: textEdit.hoveredLink + readonly property alias preeditText: textEdit.preeditText + readonly property alias selectedText: textEdit.selectedText + readonly property alias cursorRectangle: textEdit.cursorRectangle + readonly property alias cursorSelection: textEdit.cursorSelection + readonly property alias effectiveHorizontalAlignment: textEdit.effectiveHorizontalAlignment + readonly property alias textDocument: textEdit.textDocument + + signal editingFinished() + signal clicked() + signal linkActivated(string link) + signal linkHovered(string link) + +//BEGIN TextArea dummy entries + property var flickable: undefined + property var placeholderText: undefined + property var placeholderTextColor: undefined + + signal pressAndHold(MouseEvent event) + signal pressed(MouseEvent event) + signal released(MouseEvent event) +//END TextArea dummy entries + + contentItem: TextEdit { + id: textEdit + + /** + * @brief This property holds the cursor shape that will appear whenever + * the mouse is hovering over the label. + * + * default: @c Qt.IBeamCursor + * + * @property Qt::CursorShape cursorShape + */ + property alias cursorShape: hoverHandler.cursorShape + + activeFocusOnTab: root.activeFocusOnTab + color: Kirigami.Theme.textColor + readOnly: true + selectByMouse: true + padding: 0 + selectedTextColor: Kirigami.Theme.highlightedTextColor + selectionColor: Kirigami.Theme.highlightColor + textFormat: TextEdit.AutoText + verticalAlignment: TextEdit.AlignTop + wrapMode: TextEdit.WordWrap + font: root.font + persistentSelection: contextMenu.visible + + onLinkActivated: root.linkActivated(textEdit.hoveredLink) + onLinkHovered: root.linkHovered(textEdit.hoveredLink) + onEditingFinished: root.editingFinished() + + HoverHandler { + id: hoverHandler + // By default HoverHandler accepts the left button while it shouldn't accept anything, + // causing https://bugreports.qt.io/browse/QTBUG-106489. + // Qt.NoButton unfortunately is not a valid value for acceptedButtons. + // Disabling masks the problem, but + // there is no proper workaround other than an upstream fix + // See qqc2-desktop-style Label.qml + enabled: false + cursorShape: root.cursorShape ? root.cursorShape : (textEdit.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor) + } + + TapHandler { + // For custom click actions we want selection to be turned off + enabled: !textEdit.selectByMouse + + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus + acceptedButtons: Qt.LeftButton + + onTapped: root.clicked() + } + + TapHandler { + enabled: textEdit.selectByMouse && root.contextMenuEnabled + + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus + acceptedButtons: Qt.RightButton + + onPressedChanged: if (pressed) { + contextMenu.popup(); + } + } + + QQC2.Menu { + id: contextMenu + QQC2.MenuItem { + action: T.Action { + icon.name: "edit-copy-symbolic" + text: qsTr("Copy") + shortcut: StandardKey.Copy + enabled: root.visible && textEdit.selectedText.length > 0 + } + onTriggered: { + textEdit.copy(); + textEdit.deselect(); + } + } + QQC2.MenuSeparator {} + QQC2.MenuItem { + action: T.Action { + icon.name: "edit-select-all-symbolic" + text: qsTr("Select All") + shortcut: StandardKey.SelectAll + enabled: root.visible + } + onTriggered: { + textEdit.selectAll(); + } + } + } + } +} diff --git a/src/controls/SwipeListItem.qml b/src/controls/SwipeListItem.qml new file mode 100644 index 0000000..5a84654 --- /dev/null +++ b/src/controls/SwipeListItem.qml @@ -0,0 +1,641 @@ +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami +import "private" + +/** + * An item delegate intended to support extra actions obtainable + * by uncovering them by dragging away the item with the handle. + * + * This acts as a container for normal list items. + * + * Example usage: + * @code + * ListView { + * model: myModel + * delegate: SwipeListItem { + * QQC2.Label { + * text: model.text + * } + * actions: [ + * Action { + * icon.name: "document-decrypt" + * onTriggered: print("Action 1 clicked") + * }, + * Action { + * icon.name: model.action2Icon + * onTriggered: //do something + * } + * ] + * } + * + * } + * @endcode + * + * @inherit QtQuick.Templates.SwipeDelegate + */ +QQC2.SwipeDelegate { + id: listItem + +//BEGIN properties + /** + * @brief This property sets whether the item should emit signals related to mouse interaction. + * + * default: ``true`` + * + * @deprecated Use hoverEnabled instead. + * @property bool supportsMouseEvents + */ + property alias supportsMouseEvents: listItem.hoverEnabled + + /** + * @brief This property tells whether the cursor is currently hovering over the item. + * + * On mobile touch devices, this will be true only when pressed. + * + * @see QtQuick.Templates.ItemDelegate::hovered + * @deprecated This will be removed in KF6; use the ``hovered`` property instead. + * @property bool containsMouse + */ + readonly property alias containsMouse: listItem.hovered + + /** + * @brief This property sets whether instances of this list item will alternate + * between two colors, helping readability. + * + * It is suggested to use this only when implementing a view with multiple columns. + * + * default: ``false`` + * + * @since 2.7 + */ + property bool alternatingBackground: false + + /** + * @brief This property sets whether this item is a section delegate. + * + * Setting this to true will make the list item look like a "title" for items under it. + * + * default: ``false`` + * + * @see ListSectionHeader + */ + property bool sectionDelegate: false + + /** + * @brief This property sets whether the separator is visible. + * + * The separator is a line between this and the item under it. + * + * default: ``false`` + */ + property bool separatorVisible: false + + /** + * @brief This property holds the background color of the list item. + * + * It is advised to use the default value. + * default: ``Kirigami.Theme.backgroundColor`` + */ + property color backgroundColor: Kirigami.Theme.backgroundColor + + /** + * @brief This property holds the background color to be used when + * background alternating is enabled. + * + * It is advised to use the default value. + * default: ``Kirigami.Theme.alternateBackgroundColor`` + * + * @since 2.7 + */ + property color alternateBackgroundColor: Kirigami.Theme.alternateBackgroundColor + + /** + * @brief This property holds the color of the background + * when the item is pressed or selected. + * + * It is advised to use the default value. + * default: ``Kirigami.Theme.highlightColor`` + */ + property color activeBackgroundColor: Kirigami.Theme.highlightColor + + /** + * @brief This property holds the color of the text in the item. + * + * It is advised to use the default value. + * default: ``Theme.textColor`` + * + * If custom text elements are inserted in a SwipeListItem, + * their color will have to be manually set with this property. + */ + property color textColor: Kirigami.Theme.textColor + + /** + * @brief This property holds the color of the text when the item is pressed or selected. + * + * It is advised to use the default value. + * default: ``Kirigami.Theme.highlightedTextColor`` + * + * If custom text elements are inserted in a SwipeListItem, + * their color property will have to be manually bound with this property + */ + property color activeTextColor: Kirigami.Theme.highlightedTextColor + + /** + * @brief This property tells whether actions are visible and interactive. + * + * True if it's possible to see and interact with the item's actions. + * + * Actions become hidden while editing of an item, for example. + * + * @since 2.5 + */ + readonly property bool actionsVisible: actionsLayout.hasVisibleActions + + /** + * @brief This property sets whether actions behind this SwipeListItem will always be visible. + * + * default: `true in desktop and tablet mode` + * + * @since 2.15 + */ + property bool alwaysVisibleActions: !Kirigami.Settings.isMobile + + /** + * @brief This property holds actions of the list item. + * + * At most 4 actions can be revealed when sliding away the list item; + * others will be shown in the overflow menu. + */ + property list actions + + /** + * @brief This property holds the width of the overlay. + * + * The value can represent the width of the handle component or the action layout. + * + * @since 2.19 + * @property real overlayWidth + */ + readonly property alias overlayWidth: overlayLoader.width + +//END properties + + property ListView listView; + + LayoutMirroring.childrenInherit: true + + hoverEnabled: true + implicitWidth: contentItem ? implicitContentWidth : Kirigami.Units.gridUnit * 12 + width: parent ? parent.width : implicitWidth + implicitHeight: Math.max(Kirigami.Units.gridUnit * 2, implicitContentHeight) + topPadding + bottomPadding + + padding: !listItem.alwaysVisibleActions && Kirigami.Settings.tabletMode ? Kirigami.Units.largeSpacing : Kirigami.Units.smallSpacing + + leftPadding: padding * 2 + (mirrored ? overlayLoader.paddingOffset : 0) + rightPadding: padding * 2 + (mirrored ? 0 : overlayLoader.paddingOffset) + + topPadding: padding + bottomPadding: padding + + Keys.onTabPressed: (event) => { + if (actionsLayout.hasVisibleActions) { + actionsLayout.children[0].tabbedFromDelegate = true + actionsLayout.children[0].forceActiveFocus(Qt.TabFocusReason) + } else { + event.accepted = false + } + } + + Keys.onPressed: (event) => { + if ((actionsLayout.hasVisibleActions && activeFocus && event.key == Qt.Key_Right && Qt.application.layoutDirection == Qt.LeftToRight) || + (actionsLayout.hasVisibleActions && activeFocus && event.key == Qt.Key_Left && Qt.application.layoutDirection == Qt.RightToLeft)) { + for (var target = 0; target < actionsRep.count; target ++) { + if (actionsLayout.children[target].visible) { + break + } + } + if (target < actionsRep.count) { + actionsLayout.children[target].forceActiveFocus(Qt.TabFocusReason) + event.accepted = true + } + } + } + + QtObject { + id: internal + + property Flickable view: listItem.ListView.view || (listItem.parent ? (listItem.parent.ListView.view || (listItem.parent instanceof Flickable ? listItem.parent : null)) : null) + + function viewHasPropertySwipeFilter(): bool { + return view && view.parent && view.parent.parent && "_swipeFilter" in view.parent.parent; + } + + readonly property QtObject swipeFilterItem: (viewHasPropertySwipeFilter() && view.parent.parent._swipeFilter) ? view.parent.parent._swipeFilter : null + + readonly property bool edgeEnabled: swipeFilterItem ? swipeFilterItem.currentItem === listItem || swipeFilterItem.currentItem === listItem.parent : false + + // install the SwipeItemEventFilter + onViewChanged: { + if (listItem.alwaysVisibleActions || !Kirigami.Settings.tabletMode) { + return; + } + if (viewHasPropertySwipeFilter() && Kirigami.Settings.tabletMode && !internal.view.parent.parent._swipeFilter) { + const component = Qt.createComponent(Qt.resolvedUrl("../private/SwipeItemEventFilter.qml")); + internal.view.parent.parent._swipeFilter = component.createObject(internal.view.parent.parent); + component.destroy(); + } + } + } + + Connections { + target: Kirigami.Settings + function onTabletModeChanged() { + if (!internal.viewHasPropertySwipeFilter()) { + return; + } + if (Kirigami.Settings.tabletMode) { + if (!internal.swipeFilterItem) { + const component = Qt.createComponent(Qt.resolvedUrl("../private/SwipeItemEventFilter.qml")); + listItem.ListView.view.parent.parent._swipeFilter = component.createObject(listItem.ListView.view.parent.parent); + component.destroy(); + } + } else { + if (listItem.ListView.view.parent.parent._swipeFilter) { + listItem.ListView.view.parent.parent._swipeFilter.destroy(); + slideAnim.to = 0; + slideAnim.restart(); + } + } + } + } + +//BEGIN items + Loader { + id: overlayLoader + readonly property int paddingOffset: (visible ? width : 0) + Kirigami.Units.smallSpacing + readonly property var theAlias: anchors + function validate(want, defaultValue) { + const expectedLeftPadding = () => listItem.padding * 2 + (listItem.mirrored ? overlayLoader.paddingOffset : 0) + const expectedRightPadding = () => listItem.padding * 2 + (listItem.mirrored ? 0 : overlayLoader.paddingOffset) + + const warningText = + `Don't override the leftPadding or rightPadding on a SwipeListItem!\n` + + `This makes it impossible for me to adjust my layout as I need to for various usecases.\n` + + `I'll try to fix the mistake for you, but you should remove your overrides from your app's code entirely.\n` + + `If I can't fix the paddings, I'll fall back to a default layout, but it'll be slightly incorrect and lacks\n` + + `adaptations needed for touch screens and right-to-left languages, among other things.` + + if (listItem.leftPadding != expectedLeftPadding() || listItem.rightPadding != expectedRightPadding()) { + listItem.leftPadding = Qt.binding(expectedLeftPadding) + listItem.rightPadding = Qt.binding(expectedRightPadding) + console.warn(warningText) + return defaultValue + } + + return want + } + anchors { + right: validate(listItem.mirrored ? undefined : (contentItem ? contentItem.right : undefined), contentItem ? contentItem.right : undefined) + rightMargin: validate(-paddingOffset, 0) + left: validate(!listItem.mirrored ? undefined : (contentItem ? contentItem.left : undefined), undefined) + leftMargin: validate(-paddingOffset, 0) + top: parent.top + bottom: parent.bottom + } + LayoutMirroring.enabled: false + + parent: listItem + z: contentItem ? contentItem.z + 1 : 0 + width: item ? item.implicitWidth : actionsLayout.implicitWidth + active: !listItem.alwaysVisibleActions && Kirigami.Settings.tabletMode + visible: listItem.actionsVisible && opacity > 0 + asynchronous: true + sourceComponent: handleComponent + opacity: listItem.alwaysVisibleActions || Kirigami.Settings.tabletMode || listItem.hovered ? 1 : 0 + Behavior on opacity { + OpacityAnimator { + id: opacityAnim + duration: Kirigami.Units.veryShortDuration + easing.type: Easing.InOutQuad + } + } + } + + Component { + id: handleComponent + + MouseArea { + id: dragButton + anchors { + right: parent.right + } + implicitWidth: Kirigami.Units.iconSizes.smallMedium + + preventStealing: true + readonly property real openPosition: (listItem.width - width - listItem.leftPadding * 2)/listItem.width + property real startX: 0 + property real lastPosition: 0 + property bool openIntention + + onPressed: mouse => { + startX = mapToItem(listItem, 0, 0).x; + } + onClicked: mouse => { + if (Math.abs(mapToItem(listItem, 0, 0).x - startX) > Qt.styleHints.startDragDistance) { + return; + } + if (listItem.mirrored) { + if (listItem.swipe.position < 0.5) { + slideAnim.to = openPosition + } else { + slideAnim.to = 0 + } + } else { + if (listItem.swipe.position > -0.5) { + slideAnim.to = -openPosition + } else { + slideAnim.to = 0 + } + } + slideAnim.restart(); + } + onPositionChanged: mouse => { + const pos = mapToItem(listItem, mouse.x, mouse.y); + + if (listItem.mirrored) { + listItem.swipe.position = Math.max(0, Math.min(openPosition, (pos.x / listItem.width))); + openIntention = listItem.swipe.position > lastPosition; + } else { + listItem.swipe.position = Math.min(0, Math.max(-openPosition, (pos.x / (listItem.width -listItem.rightPadding) - 1))); + openIntention = listItem.swipe.position < lastPosition; + } + lastPosition = listItem.swipe.position; + } + onReleased: mouse => { + if (listItem.mirrored) { + if (openIntention) { + slideAnim.to = openPosition + } else { + slideAnim.to = 0 + } + } else { + if (openIntention) { + slideAnim.to = -openPosition + } else { + slideAnim.to = 0 + } + } + slideAnim.restart(); + } + + Kirigami.Icon { + id: handleIcon + anchors.fill: parent + selected: listItem.checked || (listItem.down && !listItem.checked && !listItem.sectionDelegate) + source: (listItem.mirrored ? (listItem.background.x < listItem.background.width/2 ? "overflow-menu-right" : "overflow-menu-left") : (listItem.background.x < -listItem.background.width/2 ? "overflow-menu-right" : "overflow-menu-left")) + } + + Connections { + id: swipeFilterConnection + + target: internal.edgeEnabled ? internal.swipeFilterItem : null + function onPeekChanged() { + if (!listItem.actionsVisible) { + return; + } + + if (listItem.mirrored) { + listItem.swipe.position = Math.max(0, Math.min(dragButton.openPosition, internal.swipeFilterItem.peek)); + dragButton.openIntention = listItem.swipe.position > dragButton.lastPosition; + + } else { + listItem.swipe.position = Math.min(0, Math.max(-dragButton.openPosition, -internal.swipeFilterItem.peek)); + dragButton.openIntention = listItem.swipe.position < dragButton.lastPosition; + } + + dragButton.lastPosition = listItem.swipe.position; + } + function onPressed(mouse) { + if (internal.edgeEnabled) { + dragButton.pressed(mouse); + } + } + function onClicked(mouse) { + if (Math.abs(listItem.background.x) < Kirigami.Units.gridUnit && internal.edgeEnabled) { + dragButton.clicked(mouse); + } + } + function onReleased(mouse) { + if (internal.edgeEnabled) { + dragButton.released(mouse); + } + } + function onCurrentItemChanged() { + if (!internal.edgeEnabled) { + slideAnim.to = 0; + slideAnim.restart(); + } + } + } + } + } + + // TODO: expose in API? + Component { + id: actionsBackgroundDelegate + Item { + anchors.fill: parent + z: 1 + + readonly property Item contentItem: swipeBackground + Rectangle { + id: swipeBackground + anchors { + top: parent.top + bottom: parent.bottom + } + clip: true + color: parent.pressed ? Qt.darker(Kirigami.Theme.backgroundColor, 1.1) : Qt.darker(Kirigami.Theme.backgroundColor, 1.05) + x: listItem.mirrored ? listItem.background.x - width : (listItem.background.x + listItem.background.width) + width: listItem.mirrored ? parent.width - (parent.width - x) : parent.width - x + + TapHandler { + onTapped: listItem.swipe.close() + } + EdgeShadow { + edge: Qt.TopEdge + visible: background.x != 0 + anchors { + right: parent.right + left: parent.left + top: parent.top + } + } + EdgeShadow { + edge: listItem.mirrored ? Qt.RightEdge : Qt.LeftEdge + + visible: background.x != 0 + anchors { + top: parent.top + bottom: parent.bottom + } + } + } + + visible: listItem.swipe.position != 0 + } + } + + + RowLayout { + id: actionsLayout + + LayoutMirroring.enabled: listItem.mirrored + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + rightMargin: Kirigami.Units.smallSpacing + } + visible: parent !== listItem + parent: !listItem.alwaysVisibleActions && Kirigami.Settings.tabletMode + ? listItem.swipe.leftItem?.contentItem || listItem.swipe.rightItem?.contentItem || listItem + : overlayLoader + + property bool hasVisibleActions: false + property int indexInListView: index ?? -1 // might not be set if using required properties + + function updateVisibleActions(definitelyVisible: bool) { + hasVisibleActions = definitelyVisible || listItem.actions.some(isActionVisible); + } + + function isActionVisible(action: T.Action): bool { + return (action instanceof Kirigami.Action) ? action.visible : true; + } + + Repeater { + id: actionsRep + model: listItem.actions + + delegate: QQC2.ToolButton { + required property T.Action modelData + required property int index + + property bool tabbedFromDelegate: false + + action: modelData + display: T.AbstractButton.IconOnly + visible: actionsLayout.isActionVisible(action) + + onVisibleChanged: actionsLayout.updateVisibleActions(visible); + Component.onCompleted: actionsLayout.updateVisibleActions(visible); + Component.onDestruction: actionsLayout.updateVisibleActions(visible); + + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + QQC2.ToolTip.visible: (Kirigami.Settings.tabletMode ? pressed : hovered) && QQC2.ToolTip.text.length > 0 + QQC2.ToolTip.text: (action as Kirigami.Action)?.tooltip ?? action?.text ?? "" + + onClicked: { + slideAnim.to = 0; + slideAnim.restart(); + } + + Keys.onBacktabPressed: (event) => { + if (tabbedFromDelegate) { + listItem.forceActiveFocus(Qt.BacktabFocusReason) + } else { + event.accepted = false + } + } + + Keys.onPressed: (event) => { + if ((Qt.application.layoutDirection == Qt.LeftToRight && event.key == Qt.Key_Left) || + (Qt.application.layoutDirection == Qt.RightToLeft && event.key == Qt.Key_Right)) { + for (var target = index -1; target>=0; target--) { + if (target == -1 || actionsLayout.children[target].visible) { + break + } + } + if (target == -1) { + listItem.forceActiveFocus(Qt.BacktabFocusReason) + } else { + actionsLayout.children[target].tabbedFromDelegate = tabbedFromDelegate + actionsLayout.children[target].forceActiveFocus(Qt.TabFocusReason) + } + event.accepted = true + } else if ((Qt.application.layoutDirection == Qt.LeftToRight && event.key == Qt.Key_Right) || + (Qt.application.layoutDirection == Qt.RightToLeft && event.key == Qt.Key_Left)) { + var found=false + for (var target = index +1; target { + if (listview && actionsLayout.indexInListView >= 0) { + listView.currentIndex = actionsLayout.indexInListView + } + event.accepted = false // pass to ListView + } + + Keys.onDownPressed: (event) => { + if (listView && actionsLayout.indexInListView >= 0) { + listView.currentIndex = actionsLayout.indexInListView + } + event.accepted = false // pass to ListView + } + + onActiveFocusChanged: { + if (focus && listView) { + listView.positionViewAtIndex(actionsLayout.indexInListView, ListView.Contain) + } else if (!focus) { + tabbedFromDelegate = false + } + } + + Accessible.name: text + Accessible.description: (action as Kirigami.Action)?.tooltip ?? "" + } + } + } + + swipe { + enabled: false + right: listItem.alwaysVisibleActions || listItem.mirrored || !Kirigami.Settings.tabletMode ? null : actionsBackgroundDelegate + left: listItem.alwaysVisibleActions || listItem.mirrored && Kirigami.Settings.tabletMode ? actionsBackgroundDelegate : null + } + NumberAnimation { + id: slideAnim + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + target: listItem.swipe + property: "position" + from: listItem.swipe.position + } +//END items + + Component.onCompleted: { + listView: { + for (var targetItem = listItem; (targetItem.ListView.view === null); targetItem = targetItem.parent) { + } + listView = targetItem.ListView.view + } + } +} diff --git a/src/controls/UrlButton.qml b/src/controls/UrlButton.qml new file mode 100644 index 0000000..053d973 --- /dev/null +++ b/src/controls/UrlButton.qml @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami +import org.kde.kirigami.private as KirigamiPrivate +import QtQuick.Controls as QQC2 + +/** + * @brief A link button that contains a URL. + * + * It will open the url by default, allow to copy it if triggered with the + * secondary mouse button. + * + * @since 5.63 + * @since org.kde.kirigami 2.6 + * @inherit QtQuick.LinkButton + */ +Kirigami.LinkButton { + id: button + + /** + * This property holds the url used by the link button. + */ + property string url + + /** + * This property holds whether the url is an external link. + * + * External links will have a small icon on their right to show that the link goes to an external website. + * + * default: true + * @since 6.11 + */ + property bool externalLink: true + + text: url + enabled: url.length > 0 + visible: text.length > 0 + acceptedButtons: Qt.LeftButton | Qt.RightButton + + Accessible.name: text + Accessible.description: text !== url + ? qsTr("Open link %1", "@info:whatsthis").arg(url) + : qsTr("Open link", "@info:whatsthis") + + rightPadding: LayoutMirroring.enabled || !icon.visible ? 0 : icon.size + Kirigami.Units.smallSpacing + leftPadding: LayoutMirroring.enabled && icon.visible ? icon.size + Kirigami.Units.smallSpacing : 0 + + Kirigami.Icon { + id: icon + + readonly property int size: Kirigami.Units.iconSizes.sizeForLabels + + x: LayoutMirroring.enabled ? button.width - button.implicitWidth : button.implicitWidth - size + width: size + height: size + + visible: button.externalLink && button.url.length > 0 + + source: "open-link-symbolic" + fallback: "link-symbolic" + color: button.color + + anchors.verticalCenter: button.verticalCenter + } + + onPressed: mouse => { + if (mouse.button === Qt.RightButton) { + menu.popup(); + } + } + + onClicked: mouse => { + if (mouse.button !== Qt.RightButton) { + Qt.openUrlExternally(url); + } + } + + QQC2.ToolTip.visible: button.text !== button.url && button.url.length > 0 && button.mouseArea.containsMouse + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + QQC2.ToolTip.text: button.url + + QQC2.Menu { + id: menu + QQC2.MenuItem { + text: qsTr("Copy Link to Clipboard") + icon.name: "edit-copy" + onClicked: KirigamiPrivate.CopyHelperPrivate.copyTextToClipboard(button.url) + } + } +} diff --git a/src/controls/private/ActionIconGroup.qml b/src/controls/private/ActionIconGroup.qml new file mode 100644 index 0000000..2531b0e --- /dev/null +++ b/src/controls/private/ActionIconGroup.qml @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQml +import QtQuick + +QtObject { + property string name + property string source + property int width + property int height + property color color: Qt.rgba(0, 0, 0, 0) +} + diff --git a/src/controls/private/ActionMenuItem.qml b/src/controls/private/ActionMenuItem.qml new file mode 100644 index 0000000..96a0472 --- /dev/null +++ b/src/controls/private/ActionMenuItem.qml @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +QQC2.MenuItem { + visible: !(action instanceof Kirigami.Action) || action.visible + autoExclusive: action instanceof Kirigami.Action && action.autoExclusive + height: visible ? implicitHeight : 0 + + QQC2.ToolTip.text: (action instanceof Kirigami.Action) ? action.tooltip : "" + QQC2.ToolTip.visible: hovered && QQC2.ToolTip.text.length > 0 + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + + Accessible.onPressAction: action.trigger() +} diff --git a/src/controls/private/ActionsMenu.qml b/src/controls/private/ActionsMenu.qml new file mode 100644 index 0000000..a524683 --- /dev/null +++ b/src/controls/private/ActionsMenu.qml @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami + +QQC2.Menu { + id: root + + property alias actions: actionsInstantiator.model + + property Component submenuComponent + property Component itemDelegate: ActionMenuItem {} + property Component separatorDelegate: QQC2.MenuSeparator { + property T.Action action + } + property Component loaderDelegate: Loader { + property T.Action action + } + property T.Action parentAction + property T.MenuItem parentItem + + Instantiator { + id: actionsInstantiator + + active: root.visible + delegate: QtObject { + readonly property T.Action action: modelData + + property QtObject item: null + property bool isSubMenu: false + + Component.onCompleted: { + const isKirigamiAction = action instanceof Kirigami.Action; + if (!isKirigamiAction || action.children.length === 0) { + if (isKirigamiAction && action.separator) { + item = root.separatorDelegate.createObject(null, { action }); + } else if (action.displayComponent) { + item = root.loaderDelegate.createObject(null, { + action, + sourceComponent: action.displayComponent, + }); + } else { + item = root.itemDelegate.createObject(null, { action }); + } + root.addItem(item) + } else if (root.submenuComponent) { + item = root.submenuComponent.createObject(null, { + parentAction: action, + title: action.text, + actions: action.children, + submenuComponent: root.submenuComponent, + }); + + root.insertMenu(root.count, item); + item.parentItem = root.contentData[root.contentData.length - 1]; + isSubMenu = true; + } + } + + Component.onDestruction: { + if (isSubMenu) { + root.removeMenu(item); + } else { + root.removeItem(item); + } + item.destroy(); + } + } + } +} diff --git a/src/controls/private/BannerImage.qml b/src/controls/private/BannerImage.qml new file mode 100644 index 0000000..65ea9a6 --- /dev/null +++ b/src/controls/private/BannerImage.qml @@ -0,0 +1,224 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +/** + * This Component is used as the header of GlobalDrawer and as the header + * of Card, It can be accessed there as a grouped property but can never + * be instantiated directly. + * \private + */ +Kirigami.ShadowedImage { + id: root + +//BEGIN properties + /** + * @brief This property holds an icon to be displayed alongside the title. + * + * It can be a QIcon, a FreeDesktop-compatible icon name, or any URL understood by QtQuick.Image. + * + * @property var titleIcon + */ + property alias titleIcon: headingIcon.source + + /** + * @brief This property holds the title's text which is to be displayed on top. + * of the image. + * @see QtQuick.Text::text + * @property string title + */ + property alias title: heading.text + + /** + * @brief This property holds the title's position. + * + * default: ``Qt.AlignTop | Qt.AlignLeft`` + * + * @property Qt::Alignment titleAlignment + */ + property int titleAlignment: Qt.AlignTop | Qt.AlignLeft + + /** + * @brief This property holds the title's level. + * + * Available text size values range from 1 (largest) to 5 (smallest). + * + * default: ``1`` + * + * @see org::kde::kirigami::Heading::level + * @property int titleLevel + */ + property alias titleLevel: heading.level + + /** + * @brief This property holds the title's wrap mode. + * + * default: ``Text.NoWrap`` + * + * @see QtQuick.Text::wrapMode + * @property int titleWrapMode + */ + property alias titleWrapMode: heading.wrapMode + + /** + * @brief This property holds whether the title is part of an item considered + * checkable. + * + * If true, a checkbox will appear in the top-right corner of the title area. + * + * default: false + * + * @property bool checkable + */ + property bool checkable: false + + /** + * @brief This property holds whether the title's checkbox is currently checked. + * + * If using this outside of a GlobalDrawer or a Card, you must manually bind + * this to the checked condition of the parent item, or whatever else in your + * UI signals checkability. You must also handle the `toggled` signal when + * the user manually clicks the checkbox. + * + * default: false + * + * @property bool checked + */ + property bool checked: false + + property int leftPadding: headingIcon.valid ? Kirigami.Units.smallSpacing * 2 : Kirigami.Units.largeSpacing + property int topPadding: headingIcon.valid ? Kirigami.Units.smallSpacing * 2 : Kirigami.Units.largeSpacing + property int rightPadding: headingIcon.valid ? Kirigami.Units.smallSpacing * 2 : Kirigami.Units.largeSpacing + property int bottomPadding: headingIcon.valid ? Kirigami.Units.smallSpacing * 2 : Kirigami.Units.largeSpacing + + property int implicitWidth: Layout.preferredWidth + + readonly property bool empty: title.length === 0 && // string + source.toString().length === 0 && // QUrl + !titleIcon // QVariant hanled by Kirigami.Icon +//END properties + + signal toggled(bool checked) + + Layout.fillWidth: true + + Layout.preferredWidth: titleLayout.implicitWidth || sourceSize.width + Layout.preferredHeight: titleLayout.completed && source.toString().length > 0 ? width/(sourceSize.width / sourceSize.height) : Layout.minimumHeight + Layout.minimumHeight: titleLayout.implicitHeight > 0 ? titleLayout.implicitHeight + Kirigami.Units.smallSpacing * 2 : 0 + + onTitleAlignmentChanged: { + // VERTICAL ALIGNMENT + titleLayout.anchors.top = undefined + titleLayout.anchors.verticalCenter = undefined + titleLayout.anchors.bottom = undefined + shadowedRectangle.anchors.top = undefined + shadowedRectangle.anchors.verticalCenter = undefined + shadowedRectangle.anchors.bottom = undefined + + if (root.titleAlignment & Qt.AlignTop) { + titleLayout.anchors.top = root.top + shadowedRectangle.anchors.top = root.top + } + else if (root.titleAlignment & Qt.AlignVCenter) { + titleLayout.anchors.verticalCenter = root.verticalCenter + shadowedRectangle.anchors.verticalCenter = root.verticalCenter + } + else if (root.titleAlignment & Qt.AlignBottom) { + titleLayout.anchors.bottom = root.bottom + shadowedRectangle.anchors.bottom = root.bottom + } + + // HORIZONTAL ALIGNMENT + titleLayout.anchors.left = undefined + titleLayout.anchors.horizontalCenter = undefined + titleLayout.anchors.right = undefined + if (root.titleAlignment & Qt.AlignRight) { + titleLayout.anchors.right = root.right + } + else if (root.titleAlignment & Qt.AlignHCenter) { + titleLayout.anchors.horizontalCenter = root.horizontalCenter + } + else if (root.titleAlignment & Qt.AlignLeft) { + titleLayout.anchors.left = root.left + } + } + fillMode: Image.PreserveAspectCrop + asynchronous: true + + color: "transparent" + + Component.onCompleted: { + titleLayout.completed = true; + } + + Kirigami.ShadowedRectangle { + id: shadowedRectangle + anchors { + left: parent.left + right: parent.right + } + height: Math.min(parent.height, titleLayout.height * 1.5) + + opacity: 0.5 + color: "black" + + corners.topLeftRadius: root.titleAlignment & Qt.AlignTop ? root.corners.topLeftRadius : 0 + corners.topRightRadius: root.titleAlignment & Qt.AlignTop ? root.corners.topRightRadius : 0 + corners.bottomLeftRadius: root.titleAlignment & Qt.AlignBottom ? root.corners.bottomLeftRadius : 0 + corners.bottomRightRadius: root.titleAlignment & Qt.AlignBottom ? root.corners.bottomRightRadius : 0 + + visible: root.source.toString().length !== 0 && root.title.length !== 0 && ((root.titleAlignment & Qt.AlignTop) || (root.titleAlignment & Qt.AlignVCenter) || (root.titleAlignment & Qt.AlignBottom)) + } + + RowLayout { + id: titleLayout + property bool completed: false + anchors { + leftMargin: root.leftPadding + topMargin: root.topPadding + rightMargin: root.rightPadding + bottomMargin: root.bottomPadding + } + width: Math.min(implicitWidth, parent.width -root.leftPadding -root.rightPadding - (checkboxLoader.active ? Kirigami.Units.largeSpacing : 0)) + height: Math.min(implicitHeight, parent.height -root.topPadding -root.bottomPadding) + Kirigami.Icon { + id: headingIcon + Layout.minimumWidth: Kirigami.Units.iconSizes.large + Layout.minimumHeight: width + visible: valid + isMask: false + } + Kirigami.Heading { + id: heading + Layout.fillWidth: true + Layout.fillHeight: true + verticalAlignment: Text.AlignVCenter + visible: text.length > 0 + level: 1 + color: root.source.toString().length > 0 ? "white" : Kirigami.Theme.textColor + wrapMode: Text.NoWrap + elide: Text.ElideRight + } + } + + Loader { + id: checkboxLoader + anchors { + top: parent.top + right: parent.right + topMargin: root.topPadding + } + active: root.checkable + sourceComponent: QQC2.CheckBox { + checked: root.checked + onToggled: root.toggled(checked); + } + } +} diff --git a/src/controls/private/ContextDrawerActionItem.qml b/src/controls/private/ContextDrawerActionItem.qml new file mode 100644 index 0000000..0b334e4 --- /dev/null +++ b/src/controls/private/ContextDrawerActionItem.qml @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +QQC2.ItemDelegate { + id: listItem + + required property T.Action tAction + + readonly property Kirigami.Action kAction: tAction instanceof Kirigami.Action ? tAction : null + + readonly property bool isSeparator: kAction?.separator ?? false + readonly property bool isExpandable: kAction?.expandible ?? false + + checked: tAction.checked || (actionsMenu && actionsMenu.visible) + highlighted: checked + icon.name: tAction.icon.name + + text: tAction.text ? tAction.text : tAction.tooltip + hoverEnabled: (!isExpandable || root.collapsed) && !Kirigami.Settings.tabletMode && !isSeparator + font.pointSize: Kirigami.Theme.defaultFont.pointSize * (isExpandable ? 1.30 : 1) + + enabled: !isExpandable && tAction.enabled + visible: kAction?.visible ?? true + opacity: enabled || isExpandable ? 1.0 : 0.6 + + Accessible.onPressAction: listItem.clicked() + + Kirigami.Separator { + id: separatorAction + + visible: listItem.isSeparator + Layout.fillWidth: true + } + + ActionsMenu { + id: actionsMenu + y: Kirigami.Settings.isMobile ? -height : listItem.height + actions: kAction?.children ?? [] + submenuComponent: ActionsMenu {} + } + + Loader { + Layout.fillWidth: true + Layout.fillHeight: true + sourceComponent: kAction?.displayComponent ?? null + onStatusChanged: { + for (const child of parent.children) { + if (child === this) { + child.visible = status === Loader.Ready; + break; + } else { + child.visible = status !== Loader.Ready; + } + } + } + Component.onCompleted: statusChanged() + } + + onPressed: { + if (kAction && kAction.children.length > 0) { + actionsMenu.open(); + } + } + onClicked: { + if (!kAction || kAction.children.length === 0) { + root.drawerOpen = false; + } + + tAction?.trigger(); + } +} diff --git a/src/controls/private/DefaultCardBackground.qml b/src/controls/private/DefaultCardBackground.qml new file mode 100644 index 0000000..24c5f59 --- /dev/null +++ b/src/controls/private/DefaultCardBackground.qml @@ -0,0 +1,106 @@ + +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ +import QtQuick +import org.kde.kirigami as Kirigami + +/** + * @brief This is the default background for Cards. + * + * It provides background feedback on hover and click events, border customizability, and the ability to change the radius of each individual corner. + * + * @inherit org::kde::kirigami::ShadowedRectangle + */ +Kirigami.ShadowedRectangle { + id: root + +//BEGIN properties + /** + * @brief This property sets whether there should be a background change on a click event. + * + * default: ``false`` + */ + property bool clickFeedback: false + + /** + * @brief This property sets whether there should be a background change on a click event. + * + * default: ``false`` + */ + property bool hoverFeedback: false + + /** + * @brief This property holds the card's normal background color. + * + * default: ``Kirigami.Theme.backgroundColor`` + */ + property color defaultColor: Kirigami.Theme.backgroundColor + + /** + * @brief This property holds the color displayed when a click event is triggered. + * @see DefaultCardBackground::clickFeedback + */ + property color pressedColor: Kirigami.ColorUtils.tintWithAlpha( + defaultColor, + Kirigami.Theme.highlightColor, 0.3) + + /** + * @brief This property holds the color displayed when a hover event is triggered. + * @see DefaultCardBackground::hoverFeedback + */ + property color hoverColor: Kirigami.ColorUtils.tintWithAlpha( + defaultColor, + Kirigami.Theme.highlightColor, 0.1) + + /** + * @brief This property holds the border width which is displayed at the edge of DefaultCardBackground. + * + * default: ``1`` + */ + property int borderWidth: 1 + + /** + * @brief This property holds the border color which is displayed at the edge of DefaultCardBackground. + */ + property color borderColor: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast) + +//END properties + + color: { + if (root.parent.checked || (root.clickFeedback && (root.parent.down || root.parent.highlighted))) + return root.pressedColor + else if (root.hoverFeedback && root.parent.hovered) + return root.hoverColor + return root.defaultColor + } + + radius: Kirigami.Units.cornerRadius + + border { + width: root.borderWidth + color: root.borderColor + } + shadow { + size: Kirigami.Units.gridUnit + color: Qt.rgba(0, 0, 0, 0.05) + yOffset: 2 + } + + // basic drop shadow + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: Math.round(Kirigami.Units.smallSpacing / 4) + + radius: root.radius + height: root.height + color: Qt.darker(Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.6), 1.1) + visible: !root.clickFeedback || !root.parent.down + + z: -1 + } +} diff --git a/src/controls/private/DefaultChipBackground.qml b/src/controls/private/DefaultChipBackground.qml new file mode 100644 index 0000000..2a2ecc3 --- /dev/null +++ b/src/controls/private/DefaultChipBackground.qml @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2022 Felipe Kinoshita +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import org.kde.kirigami as Kirigami + +Rectangle { + + /** + * @brief This property holds the chip's default background color. + */ + property color defaultColor: Kirigami.Theme.backgroundColor + + /** + * @brief This property holds the color of the Chip's background when it is being pressed. + * @see QtQuick.AbstractButton::down + */ + property color pressedColor: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.3) + + /** + * @brief This property holds the color of the Chip's background when it is checked. + * @see QtQuick.AbstractButton::checked + */ + property color checkedColor: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.2) + + /** + * @brief This property holds the chip's default border color. + */ + property color defaultBorderColor: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast) + + /** + * @brief This property holds the color of the Chip's border when it is checked. + * @see QtQuick.AbstractButton::checked + */ + property color checkedBorderColor: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.9) + + /** + * @brief This property holds the color of the Chip's border when it is being pressed. + * @see QtQuick.AbstractButton::down + */ + property color pressedBorderColor: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.7) + + /** + * @brief This property holds the color of the Chip's border when it is hovered. + * @see QtQuick.Control::hovered + */ + property color hoveredBorderColor: Kirigami.Theme.hoverColor + + Kirigami.Theme.colorSet: Kirigami.Theme.Header + Kirigami.Theme.inherit: false + + color: { + if (parent.down) { + return pressedColor + } else if (parent.checked) { + return checkedColor + } else { + return defaultColor + } + } + border.color: { + if (parent.down) { + return pressedBorderColor + } else if (parent.checked) { + return checkedBorderColor + } else if (parent.hovered) { + return hoveredBorderColor + } else { + return defaultBorderColor + } + } + border.width: 1 + radius: Kirigami.Units.cornerRadius +} diff --git a/src/controls/private/DefaultPageTitleDelegate.qml b/src/controls/private/DefaultPageTitleDelegate.qml new file mode 100644 index 0000000..1c875cf --- /dev/null +++ b/src/controls/private/DefaultPageTitleDelegate.qml @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +/** + * This component is used as a default representation for a page title within + * page's header/toolbar. It is just a Heading item with shrinking + eliding + * behavior. + * + * \private + */ +Item { + property alias text: heading.text + + Layout.fillWidth: true + Layout.minimumWidth: 0 + Layout.maximumWidth: implicitWidth + + implicitWidth: Math.ceil(heading.implicitWidth) + implicitHeight: Math.ceil(heading.implicitHeight) + + Kirigami.Heading { + id: heading + + anchors.fill: parent + maximumLineCount: 1 + elide: Text.ElideRight + textFormat: Text.PlainText + } +} diff --git a/src/controls/private/EdgeShadow.qml b/src/controls/private/EdgeShadow.qml new file mode 100644 index 0000000..523e7a4 --- /dev/null +++ b/src/controls/private/EdgeShadow.qml @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +Item { + id: shadow + /** + * @brief This property holds the edge of the shadow that will determine the direction of the gradient. + * The acceptable values are: + * * ``Qt.TopEdge``: the top edge of the content item. + * * ``Qt.LeftEdge``: the left edge of the content item + * * ``Qt.RightEdge``: the right edge of the content item. + * * ``Qt.BottomEdge``: the bottom edge of the content item. + * + * @see Qt::Edges + */ + property int edge: Qt.LeftEdge + + property int radius: Kirigami.Units.cornerRadius + implicitWidth: radius + implicitHeight: radius + + Rectangle { + x: shadow.width / 2 - width / 2 + y: shadow.height / 2 - height / 2 + width: (shadow.edge === Qt.LeftEdge || shadow.edge === Qt.RightEdge) ? shadow.height : shadow.width + height: (shadow.edge === Qt.LeftEdge || shadow.edge === Qt.RightEdge) ? shadow.width : shadow.height + rotation: { + switch (shadow.edge) { + case Qt.TopEdge: return 0; + case Qt.LeftEdge: return 270; + case Qt.RightEdge: return 90; + case Qt.BottomEdge: return 180; + } + } + + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.rgba(0, 0, 0, 0.25) + } + GradientStop { + position: 0.20 + color: Qt.rgba(0, 0, 0, 0.1) + } + GradientStop { + position: 0.35 + color: Qt.rgba(0, 0, 0, 0.02) + } + GradientStop { + position: 1.0 + color: "transparent" + } + } + } +} + diff --git a/src/controls/private/GlobalDrawerActionItem.qml b/src/controls/private/GlobalDrawerActionItem.qml new file mode 100644 index 0000000..3666d95 --- /dev/null +++ b/src/controls/private/GlobalDrawerActionItem.qml @@ -0,0 +1,207 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Controls.impl as QQC2Impl +import QtQuick.Layouts +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami + +QQC2.ItemDelegate { + id: listItem + + required property T.Action tAction + // `as` case operator is still buggy + readonly property Kirigami.Action kAction: tAction instanceof Kirigami.Action ? tAction : null + + readonly property bool actionVisible: kAction?.visible ?? true + readonly property bool isSeparator: kAction?.separator ?? false + readonly property bool isExpandable: kAction?.expandible ?? false + readonly property bool hasChildren: kAction ? kAction.children.length > 0 : false + readonly property bool hasVisibleMenu: actionsMenu?.visible ?? false + readonly property bool hasToolTip: kAction ? kAction.tooltip !== "" : false + + checked: checkedBinding() + highlighted: checked + activeFocusOnTab: true + + width: parent.width + + contentItem: RowLayout { + spacing: Kirigami.Units.largeSpacing + + Kirigami.Icon { + id: iconItem + color: listItem.tAction.icon.color + source: listItem.tAction.icon.name || listItem.tAction.icon.source + + readonly property int size: Kirigami.Units.iconSizes.smallMedium + Layout.minimumHeight: size + Layout.maximumHeight: size + Layout.minimumWidth: size + Layout.maximumWidth: size + + selected: (listItem.highlighted || listItem.checked || listItem.down) + visible: source !== undefined && !listItem.isSeparator + } + + QQC2Impl.MnemonicLabel { + id: labelItem + visible: !listItem.isSeparator + text: width > height * 2 ? listItem.Kirigami.MnemonicData.mnemonicLabel : "" + Accessible.name: listItem.Kirigami.MnemonicData.plainTextLabel + + // Work around Qt bug where left aligned text is not right aligned + // in RTL mode unless horizontalAlignment is explicitly set. + // https://bugreports.qt.io/browse/QTBUG-95873 + horizontalAlignment: Text.AlignLeft + + Layout.fillWidth: true + mnemonicVisible: listItem.Kirigami.MnemonicData.active + color: (listItem.highlighted || listItem.checked || listItem.down) ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor + elide: Text.ElideRight + font: listItem.font + opacity: { + if (root.collapsed) { + return 0; + } else if (!listItem.enabled) { + return 0.6; + } else { + return 1.0; + } + } + Behavior on opacity { + NumberAnimation { + duration: Kirigami.Units.longDuration/2 + easing.type: Easing.InOutQuad + } + } + } + + Kirigami.Separator { + id: separatorAction + + visible: listItem.isSeparator + Layout.fillWidth: true + } + + Kirigami.Icon { + isMask: true + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: !root.collapsed ? 0 : -width + Layout.preferredHeight: !root.collapsed ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.small/2 + opacity: 0.7 + selected: listItem.checked || listItem.down + Layout.preferredWidth: Layout.preferredHeight + source: listItem.mirrored ? "go-next-symbolic-rtl" : "go-next-symbolic" + visible: (!listItem.isExpandable || root.collapsed) && !listItem.isSeparator && listItem.hasChildren + } + } + + Accessible.name: Kirigami.MnemonicData.plainTextLabel + Kirigami.MnemonicData.enabled: enabled && visible + Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.MenuItem + Kirigami.MnemonicData.label: tAction?.text ?? "" + + Shortcut { + sequence: listItem.Kirigami.MnemonicData.sequence + onActivated: listItem.clicked() + } + + property ActionsMenu actionsMenu: ActionsMenu { + x: Qt.application.layoutDirection === Qt.RightToLeft ? -width : listItem.width + actions: listItem.kAction?.children ?? [] + submenuComponent: ActionsMenu {} + + onVisibleChanged: { + if (visible) { + stackView.openSubMenu = listItem.actionsMenu; + } else if (stackView.openSubMenu === listItem.actionsMenu) { + stackView.openSubMenu = null; + } + } + } + + // TODO: animate the hide by collapse + visible: actionVisible && opacity > 0 + opacity: !root.collapsed || iconItem.source.toString().length > 0 + + Behavior on opacity { + NumberAnimation { + duration: Kirigami.Units.longDuration / 2 + easing.type: Easing.InOutQuad + } + } + + enabled: tAction?.enabled ?? false + + hoverEnabled: (!isExpandable || root.collapsed) && !Kirigami.Settings.tabletMode && !isSeparator + font.pointSize: isExpandable ? Kirigami.Theme.defaultFont.pointSize * 1.30 : Kirigami.Theme.defaultFont.pointSize + height: implicitHeight * opacity + + QQC2.ToolTip { + visible: !listItem.isSeparator + && (listItem.hasToolTip || root.collapsed) + && !listItem.hasVisibleMenu + && listItem.hovered + && text.length > 0 + + text: (listItem.kAction?.tooltip || listItem.tAction?.text) ?? "" + delay: Kirigami.Units.toolTipDelay + y: (listItem.height - height) / 2 + x: Qt.application.layoutDirection === Qt.RightToLeft ? -width : listItem.width + } + + onHoveredChanged: { + if (!hovered) { + return; + } + if (stackView.openSubMenu) { + stackView.openSubMenu.visible = false; + + if (actionsMenu.count > 0) { + actionsMenu.popup(this, width, 0); + } + } + } + + onClicked: trigger() + Accessible.onPressAction: trigger() + Keys.onEnterPressed: event => trigger() + Keys.onReturnPressed: event => trigger() + + function trigger() { + tAction?.trigger(); + + if (hasChildren) { + if (root.collapsed) { + if (actionsMenu.count > 0 && !actionsMenu.visible) { + stackView.openSubMenu = actionsMenu; + actionsMenu.popup(this, width, 0); + } + } else { + stackView.push(menuComponent, { + model: kAction?.children ?? [], + level: level + 1, + current: tAction, + }); + } + } else if (root.resetMenuOnTriggered) { + root.resetMenu(); + } + checked = Qt.binding(() => checkedBinding()); + } + + function checkedBinding(): bool { + return (tAction?.checked || actionsMenu?.visible) ?? false; + } + + Keys.onDownPressed: event => nextItemInFocusChain().focus = true + Keys.onUpPressed: event => nextItemInFocusChain(false).focus = true +} diff --git a/src/controls/private/MobileDialogLayer.qml b/src/controls/private/MobileDialogLayer.qml new file mode 100644 index 0000000..7348fa4 --- /dev/null +++ b/src/controls/private/MobileDialogLayer.qml @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +Kirigami.Dialog { + id: dialog + + clip: true + modal: true + + topPadding: 0 + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + + header: Kirigami.AbstractApplicationHeader { + pageRow: null + page: null + + minimumHeight: Kirigami.Units.gridUnit * 1.6 + maximumHeight: Kirigami.Units.gridUnit * 1.6 + preferredHeight: Kirigami.Units.gridUnit * 1.6 + + Keys.onEscapePressed: event => { + if (dialog.opened) { + dialog.close(); + } else { + event.accepted = false; + } + } + + contentItem: RowLayout { + width: parent.width + Kirigami.Heading { + Layout.leftMargin: Kirigami.Units.largeSpacing + text: dialog.title + elide: Text.ElideRight + } + Item { + Layout.fillWidth: true + } + Kirigami.Icon { + id: closeIcon + Layout.alignment: Qt.AlignVCenter + Layout.rightMargin: Kirigami.Units.largeSpacing + Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium + Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium + source: closeMouseArea.containsMouse ? "window-close" : "window-close-symbolic" + active: closeMouseArea.containsMouse + MouseArea { + id: closeMouseArea + hoverEnabled: true + anchors.fill: parent + onClicked: mouse => dialog.close(); + } + } + } + } + + contentItem: QQC2.Control { + topPadding: 0 + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + } +} diff --git a/src/controls/private/PrivateActionToolButton.qml b/src/controls/private/PrivateActionToolButton.qml new file mode 100644 index 0000000..b7a38eb --- /dev/null +++ b/src/controls/private/PrivateActionToolButton.qml @@ -0,0 +1,126 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQml +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T + +import org.kde.kirigami as Kirigami + +QQC2.ToolButton { + id: control + + signal menuAboutToShow() + + hoverEnabled: true + + display: QQC2.ToolButton.TextBesideIcon + + property bool showMenuArrow: !Kirigami.DisplayHint.displayHintSet(action, Kirigami.DisplayHint.HideChildIndicator) + + property list menuActions: { + if (action instanceof Kirigami.Action) { + return action.children; + } + return [] + } + + property Component menuComponent: ActionsMenu { + submenuComponent: ActionsMenu { } + } + + property T.Menu menu: null + + // We create the menu instance only when there are any actual menu items. + // This also happens in the background, avoiding slowdowns due to menu item + // creation on the main thread. + onMenuActionsChanged: { + if (menuComponent && menuActions.length > 0) { + if (!menu) { + const setupIncubatedMenu = incubatedMenu => { + menu = incubatedMenu + // Important: We handle the press on parent in the parent, so ignore it here. + menu.closePolicy = QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutsideParent + menu.closed.connect(() => control.checked = false) + menu.actions = control.menuActions + } + const incubator = menuComponent.incubateObject(control, { actions: menuActions }) + if (incubator.status !== Component.Ready) { + incubator.onStatusChanged = status => { + if (status === Component.Ready) { + setupIncubatedMenu(incubator.object) + } + } + } else { + setupIncubatedMenu(incubator.object); + } + } else { + menu.actions = menuActions + } + } + } + + visible: action instanceof Kirigami.Action ? action.visible : true + autoExclusive: action instanceof Kirigami.Action ? action.autoExclusive : false + + // Workaround for QTBUG-85941 + Binding { + target: control + property: "checkable" + value: (control.action?.checkable ?? false) || (control.menuActions.length > 0) + restoreMode: Binding.RestoreBinding + } + + // Important: This cannot be a direct onVisibleChanged handler in the button + // because it breaks action assignment if we do that. However, this slightly + // more indirect Connections object does not have that effect. + Connections { + target: control + function onVisibleChanged() { + if (!control.visible && control.menu && control.menu.visible) { + control.menu.dismiss() + } + } + } + + onToggled: { + if (menuActions.length > 0 && menu) { + if (checked) { + control.menuAboutToShow(); + menu.popup(control, 0, control.height) + } else { + menu.dismiss() + } + } + } + + QQC2.ToolTip { + visible: control.hovered && text.length > 0 && !(control.menu && control.menu.visible) && !control.pressed + text: { + const a = control.action; + if (a) { + if (a.tooltip) { + return a.tooltip; + } else if (control.display === QQC2.Button.IconOnly) { + return a.text; + } + } + return ""; + } + } + + // This will set showMenuArrow when using qqc2-desktop-style. + Accessible.role: (control.showMenuArrow && control.menuActions.length > 0) ? Accessible.ButtonMenu : Accessible.Button + Accessible.ignored: !visible + Accessible.onPressAction: { + if (control.checkable) { + control.toggle(); + } else { + control.action.trigger(); + } + } +} diff --git a/src/controls/private/PullDownIndicator.qml b/src/controls/private/PullDownIndicator.qml new file mode 100644 index 0000000..ea1dac5 --- /dev/null +++ b/src/controls/private/PullDownIndicator.qml @@ -0,0 +1,162 @@ +/* + * SPDX-FileCopyrightText: 2023 Connor Carney + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import QtQuick.Shapes as QQShapes + +/** + * @brief A pull-down to refresh indicator that can be added to any Flickable or ScrollablePage. + */ +Item { + id: root + + //BEGIN properties + /** + * @brief The flickable that this indicator is attached to. + * + * If this is not set, the indicator will search for a Flickable in its parent chain. + */ + property Flickable flickable: { + let candidate = parent + while (candidate) { + if (candidate instanceof Flickable) { + return candidate + } else if (candidate instanceof Kirigami.ScrollablePage) { + return candidate.flickable + } + candidate = candidate.parent + } + return null; + } + + /** + * @brief Whether to show the busy indicator at the top of the flickable + * + * This should be set to true whenever a refresh is in progress. It should typically + * be set to true whe triggered() is emitted, and set to false when the refresh is + * complete. This is not done automatically because the refresh may be triggered + * from outside the indicator. + */ + property bool active: false + + /** + * @brief How far the flickable has been pulled down, between 0 (not at all) and 1 (where a refresh is triggered). + */ + readonly property real progress: !refreshing ? Math.min(-Math.min(flickable?.verticalOvershoot ?? 0, 0) / indicatorContainer.height, 1) : 0 + + /** + * @brief Time to wait after the flickable has been pulled down before triggering a refresh + * + * This gives the user a chance to back out of the refresh if they release the flickable + * before the refreshDelay has elapsed. + */ + property int refreshDelay: 500 + + /** + * @brief emitted when the flickable is pulled down far enough to trigger a refresh + */ + signal triggered() + //END properties + + Item { + id: indicatorContainer + parent: root.flickable + anchors { + bottom: parent?.contentItem?.top + bottomMargin: root.flickable.topMargin + } + + width: flickable?.width + height: Kirigami.Units.gridUnit * 4 + QQC2.BusyIndicator { + id: busyIndicator + z: 1 + anchors.centerIn: parent + running: root.active + visible: root.active + // Android busywidget QQC seems to be broken at custom sizes + } + QQShapes.Shape { + id: spinnerProgress + anchors { + fill: busyIndicator + margins: Kirigami.Units.smallSpacing + } + visible: !root.active && root.progress > 0 + QQShapes.ShapePath { + strokeWidth: Kirigami.Units.smallSpacing + strokeColor: Kirigami.Theme.highlightColor + fillColor: "transparent" + PathAngleArc { + centerX: spinnerProgress.width / 2 + centerY: spinnerProgress.height / 2 + radiusX: spinnerProgress.width / 2 - Kirigami.Units.smallSpacing / 2 + radiusY: spinnerProgress.height / 2 - Kirigami.Units.smallSpacing / 2 + startAngle: 0 + sweepAngle: 360 * root.progress + } + } + } + } + + onProgressChanged: { + if (!root.active && root.progress >= 1) { + refreshTriggerTimer.running = true; + } else { + refreshTriggerTimer.running = false; + } + } + + + states: [ + State { + name: "active" + when: root.active + PropertyChanges { + target: indicatorContainer + anchors.bottomMargin: root.flickable.topMargin - indicatorContainer.height + } + PropertyChanges { + target: root.flickable + explicit: true + + // this is not a loop because of explicit:true above + // It adds the height of the indicator to the topMargin of the flickable + // when we enter the active state; the change is automatically reversed + // when returning to the base state. + topMargin: indicatorContainer.height + root.flickable.topMargin + } + } + ] + + transitions: [ + Transition { + from: "" + to: "active" + enabled: root.flickable.verticalOvershoot >= 0 + reversible: true + NumberAnimation { + target: root.flickable + properties: "topMargin" + easing.type: Easing.InOutQuad + duration: Kirigami.Units.longDuration + } + } + ] + + Timer { + id: refreshTriggerTimer + interval: root.refreshDelay + onTriggered: { + if (!root.active && root.progress >= 1) { + root.triggered() + } + } + } + +} diff --git a/src/controls/private/SwipeItemEventFilter.qml b/src/controls/private/SwipeItemEventFilter.qml new file mode 100644 index 0000000..24f2d9a --- /dev/null +++ b/src/controls/private/SwipeItemEventFilter.qml @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + + +MouseArea { + id: swipeFilter + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + } + + z: 99999 + property Item currentItem + property real peek + + preventStealing: true + width: Kirigami.Units.gridUnit + onPressed: mouse => { + const mapped = mapToItem(parent.flickableItem.contentItem, mouse.x, mouse.y); + currentItem = parent.flickableItem.itemAt(mapped.x, mapped.y); + } + onPositionChanged: mouse => { + const mapped = mapToItem(parent.flickableItem.contentItem, mouse.x, mouse.y); + currentItem = parent.flickableItem.itemAt(mapped.x, mapped.y); + peek = 1 - mapped.x / parent.flickableItem.contentItem.width; + } +} diff --git a/src/controls/private/globaltoolbar/AbstractPageHeader.qml b/src/controls/private/globaltoolbar/AbstractPageHeader.qml new file mode 100644 index 0000000..49c378c --- /dev/null +++ b/src/controls/private/globaltoolbar/AbstractPageHeader.qml @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +Kirigami.AbstractApplicationHeader { + id: root + // anchors.fill: parent + property Item container + property bool current + + minimumHeight: pageRow ? pageRow.globalToolBar.minimumHeight : Kirigami.Units.iconSizes.medium + Kirigami.Units.smallSpacing * 2 + maximumHeight: pageRow ? pageRow.globalToolBar.maximumHeight : minimumHeight + preferredHeight: pageRow ? pageRow.globalToolBar.preferredHeight : minimumHeight + + separatorVisible: pageRow ? pageRow.globalToolBar.separatorVisible : true + + Kirigami.Theme.colorSet: pageRow ? pageRow.globalToolBar.colorSet : Kirigami.Theme.Header + + leftPadding: pageRow + ? Math.min( + width / 2, + Math.max( + (page.title.length > 0 ? pageRow.globalToolBar.titleLeftPadding : 0), + Qt.application.layoutDirection === Qt.LeftToRight + ? Math.min(pageRow.globalToolBar.leftReservedSpace, + pageRow.Kirigami.ScenePosition.x + - page.Kirigami.ScenePosition.x + + pageRow.globalToolBar.leftReservedSpace) + + Kirigami.Units.smallSpacing + : Math.min(pageRow.globalToolBar.leftReservedSpace, + -pageRow.width + + pageRow.Kirigami.ScenePosition.x + + page.Kirigami.ScenePosition.x + + page.width + + pageRow.globalToolBar.leftReservedSpace) + + Kirigami.Units.smallSpacing)) + : Kirigami.Units.smallSpacing + rightPadding: pageRow + ? Math.max(0, + Qt.application.layoutDirection === Qt.LeftToRight + ? (-pageRow.width + - pageRow.Kirigami.ScenePosition.x + + page.width + + page.Kirigami.ScenePosition.x + + pageRow.globalToolBar.rightReservedSpace) + : (pageRow.Kirigami.ScenePosition.x + - page.Kirigami.ScenePosition.x + + pageRow.globalToolBar.rightReservedSpace)) + : 0 +} diff --git a/src/controls/private/globaltoolbar/BreadcrumbControl.qml b/src/controls/private/globaltoolbar/BreadcrumbControl.qml new file mode 100644 index 0000000..a218228 --- /dev/null +++ b/src/controls/private/globaltoolbar/BreadcrumbControl.qml @@ -0,0 +1,174 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +ListView { + id: root + + readonly property Kirigami.PageRow pageRow: { + // This is fetched from breadcrumbLoader in PageRowGlobalToolBarUI.qml + const pr = parent?.pageRow ?? null; + return pr as Kirigami.PageRow; + } + + currentIndex: { + if (!pageRow) { + return -1; + } + // This ListView is eventually consistent with PageRow, so it has to + // force-refresh currentIndex when its count finally catches up, + // otherwise currentIndex might get reset and stuck at -1. + void count; + // TODO: This "eventual consistency" causes Behavior on contentX to + // scroll from the start each time a page is added. Besides, simple + // number is not the most efficient model, because ListView + // recreates all delegates when number changes. + + if (pageRow.layers.depth > 1) { + // First layer (index 0) is the main columnView. + // Since it is ignored, depth has to be adjusted by 1. + // In case of layers, current index is always the last one, + // which is one less than their count, thus minus another 1. + return pageRow.layers.depth - 2; + } else { + return pageRow.currentIndex; + } + } + + // This function exists outside of delegate, so that when popping layers + // the JavaScript execution context won't be destroyed along with delegate. + function selectIndex(index: int) { + if (!pageRow) { + return; + } + if (pageRow.layers.depth > 1) { + // First layer (index 0) is the main columnView. + // Since it is ignored, index has to be adjusted by 1. + // We want to pop anything after selected index, + // turning selected layer into current one, thus plus another 1. + while (pageRow.layers.depth > index + 2) { + pageRow.layers.pop(); + } + } else { + pageRow.currentIndex = index; + } + } + + contentHeight: height + clip: true + orientation: ListView.Horizontal + boundsBehavior: Flickable.StopAtBounds + interactive: Kirigami.Settings.hasTransientTouchInput + + contentX: { + if (!currentItem) { + return 0; + } + // preferred position: current item is centered within viewport + const preferredPosition = currentItem.x + (currentItem.width - width) / 2; + + // Note: Order of min/max is important. Make sure to test on all sorts + // and sizes before committing changes to this formula. + if (LayoutMirroring.enabled) { + // In a mirrored ListView contentX starts from left edge and increases to the left. + const maxLeftPosition = -contentWidth; + const minRightPosition = -width; + return Math.round(Math.min(minRightPosition, Math.max(preferredPosition, maxLeftPosition))); + } else { + const minLeftPosition = 0; + const maxRightPosition = contentWidth - width; + return Math.round(Math.max(minLeftPosition, Math.min(preferredPosition, maxRightPosition))); + } + } + + Behavior on contentX { + NumberAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + + model: { + if (!root.pageRow) { + return null; + } + if (root.pageRow.layers.depth > 1) { + // First layer (index 0) is the main columnView; ignore it. + return root.pageRow.layers.depth - 1; + } else { + return root.pageRow.depth; + } + } + + delegate: MouseArea { + id: delegate + + required property int index + + // We can't use Kirigami.Page here instead of Item since we now accept + // pushing PageRow to a new layer. + readonly property Item page: { + if (!root.pageRow) { + return null; + } + if (root.pageRow.layers.depth > 1) { + // First layer (index 0) is the main columnView. + // Since it is ignored, index has to be adjusted by 1. + return pageRow.layers.get(index + 1); + } else { + return pageRow.get(index); + } + } + + + width: Math.ceil(layout.implicitWidth) + height: ListView.view?.height ?? 0 + + hoverEnabled: !Kirigami.Settings.tabletMode + + onClicked: mouse => { + root.selectIndex(index); + } + + // background + Rectangle { + color: Kirigami.Theme.highlightColor + anchors.fill: parent + radius: Kirigami.Units.cornerRadius + opacity: root.count > 1 && parent.containsMouse ? 0.1 : 0 + } + + // content + RowLayout { + id: layout + anchors.fill: parent + spacing: 0 + + Kirigami.Icon { + visible: delegate.index > 0 + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: Kirigami.Units.iconSizes.small + Layout.preferredWidth: Kirigami.Units.iconSizes.small + isMask: true + color: Kirigami.Theme.textColor + source: LayoutMirroring.enabled ? "go-next-symbolic-rtl" : "go-next-symbolic" + } + Kirigami.Heading { + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing + color: Kirigami.Theme.textColor + verticalAlignment: Text.AlignVCenter + wrapMode: Text.NoWrap + text: delegate.page?.title ?? "" + opacity: delegate.ListView.isCurrentItem ? 1 : 0.4 + } + } + } +} diff --git a/src/controls/private/globaltoolbar/PageRowGlobalToolBarStyleGroup.qml b/src/controls/private/globaltoolbar/PageRowGlobalToolBarStyleGroup.qml new file mode 100644 index 0000000..7798f98 --- /dev/null +++ b/src/controls/private/globaltoolbar/PageRowGlobalToolBarStyleGroup.qml @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +QtObject { + id: globalToolBar + property int style: Kirigami.ApplicationHeaderStyle.None + + readonly property int actualStyle: { + if (style === Kirigami.ApplicationHeaderStyle.Auto) { + if (!Kirigami.Settings.isMobile) { + return Kirigami.ApplicationHeaderStyle.ToolBar + } else if (root.wideMode) { + return Kirigami.ApplicationHeaderStyle.Titles + } else { + return Kirigami.ApplicationHeaderStyle.Breadcrumb + } + } + return style; + } + + /** @property kirigami::ApplicationHeaderStyle::NavigationButtons */ + property int showNavigationButtons: (!Kirigami.Settings.isMobile || Qt.platform.os === "ios") + ? (Kirigami.ApplicationHeaderStyle.ShowBackButton | Kirigami.ApplicationHeaderStyle.ShowForwardButton) + : Kirigami.ApplicationHeaderStyle.NoNavigationButtons + property bool separatorVisible: true + //Unfortunately we can't access pageRow.globalToolbar.Kirigami.Theme directly in a declarative way + property int colorSet: Kirigami.Theme.Header + // whether or not the header should be + // "pushed" back when scrolling using the + // touch screen + property bool hideWhenTouchScrolling: false + /** + * If true, when any kind of toolbar is shown, the drawer handles will be shown inside the toolbar, if they're present + */ + property bool canContainHandles: true + property int toolbarActionAlignment: Qt.AlignRight + property int toolbarActionHeightMode: Kirigami.ToolBarLayout.ConstrainIfLarger + + property int minimumHeight: 0 + // FIXME: Figure out the exact standard size of a Toolbar + property int preferredHeight: (actualStyle === Kirigami.ApplicationHeaderStyle.ToolBar + ? Kirigami.Units.iconSizes.medium + : Kirigami.Units.gridUnit * 1.8) + Kirigami.Units.smallSpacing * 2 + property int maximumHeight: preferredHeight + + // Sets the minimum leading padding for the title in a page header + property int titleLeftPadding: Kirigami.Units.gridUnit +} diff --git a/src/controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml b/src/controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml new file mode 100644 index 0000000..5d14fce --- /dev/null +++ b/src/controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml @@ -0,0 +1,157 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami +import "../../templates" as KT +import "../../templates/private" as TP +import "../" as P + +Kirigami.AbstractApplicationHeader { + id: header + readonly property int leftReservedSpace: (buttonsLayout.visible && buttonsLayout.visibleChildren.length > 0 ? buttonsLayout.width + Kirigami.Units.smallSpacing : 0) // Take into account the layout margins the nav buttons have + + (leftHandleAnchor.visible ? leftHandleAnchor.width : 0) + + (menuButton.visible ? menuButton.width : 0) + readonly property int rightReservedSpace: rightHandleAnchor.visible ? rightHandleAnchor.width + Kirigami.Units.smallSpacing : 0 + + readonly property alias leftHandleAnchor: leftHandleAnchor + readonly property alias rightHandleAnchor: rightHandleAnchor + + readonly property bool breadcrumbVisible: layerIsMainRow && breadcrumbLoader.active + readonly property bool layerIsMainRow: (root.layers.currentItem.hasOwnProperty("columnView")) ? root.layers.currentItem.columnView === root.columnView : false + readonly property Item currentItem: layerIsMainRow ? root.columnView : root.layers.currentItem + + function __shouldHandleAnchorBeVisible(handleAnchor: Item, drawerProperty: string, itemProperty: string): bool { + if (typeof applicationWindow === "undefined") { + return false; + } + const w = applicationWindow(); + if (!w) { + return false; + } + const drawer = w[drawerProperty] as KT.OverlayDrawer; + if (!drawer || !drawer.enabled || !drawer.handleVisible || drawer.handle.handleAnchor !== handleAnchor) { + return false; + } + const item = breadcrumbLoader.pageRow?.[itemProperty] as Item; + const style = item?.globalToolBarStyle ?? Kirigami.ApplicationHeaderStyle.None; + return globalToolBar.canContainHandles || style === Kirigami.ApplicationHeaderStyle.ToolBar; + } + + height: visible ? implicitHeight : 0 + minimumHeight: globalToolBar.minimumHeight + preferredHeight: globalToolBar.preferredHeight + maximumHeight: globalToolBar.maximumHeight + separatorVisible: globalToolBar.separatorVisible + + Kirigami.Theme.colorSet: globalToolBar.colorSet + + RowLayout { + anchors.fill: parent + spacing: 0 + + Item { + Layout.preferredWidth: applicationWindow().pageStack.globalToolBar.leftReservedSpace + visible: applicationWindow().pageStack !== root + } + + Item { + id: leftHandleAnchor + visible: header.__shouldHandleAnchorBeVisible(leftHandleAnchor, "globalDrawer", "leadingVisibleItem") + + Layout.preferredHeight: Math.max(backButton.implicitHeight, parent.height) + Layout.preferredWidth: height + } + + P.PrivateActionToolButton { + id: menuButton + visible: !Kirigami.Settings.isMobile && applicationWindow().globalDrawer && "isMenu" in applicationWindow().globalDrawer && applicationWindow().globalDrawer.isMenu + icon.name: "open-menu-symbolic" + showMenuArrow: false + + Layout.preferredHeight: Math.min(backButton.implicitHeight, parent.height) + Layout.preferredWidth: height + Layout.leftMargin: Kirigami.Units.smallSpacing + + action: Kirigami.Action { + children: applicationWindow().globalDrawer && applicationWindow().globalDrawer.actions ? applicationWindow().globalDrawer.actions : [] + tooltip: checked ? qsTr("Close menu") : qsTr("Open menu") + } + Accessible.name: action.tooltip + + Connections { + // Only target the GlobalDrawer when it *is* a GlobalDrawer, since + // it can be something else, and that something else probably + // doesn't have an isMenuChanged() signal. + target: applicationWindow().globalDrawer as Kirigami.GlobalDrawer + function onIsMenuChanged() { + if (!applicationWindow().globalDrawer.isMenu && menuButton.menu) { + menuButton.menu.dismiss() + } + } + } + } + + RowLayout { + id: buttonsLayout + Layout.fillHeight: true + Layout.preferredHeight: Math.max(backButton.visible ? backButton.implicitHeight : 0, forwardButton.visible ? forwardButton.implicitHeight : 0) + + Layout.leftMargin: leftHandleAnchor.visible ? Kirigami.Units.smallSpacing : 0 + + visible: (globalToolBar.showNavigationButtons !== Kirigami.ApplicationHeaderStyle.NoNavigationButtons || applicationWindow().pageStack.layers.depth > 1 && !(applicationWindow().pageStack.layers.currentItem instanceof Kirigami.PageRow || header.layerIsMainRow)) + && globalToolBar.actualStyle !== Kirigami.ApplicationHeaderStyle.None + + Layout.maximumWidth: visibleChildren.length > 0 ? Layout.preferredWidth : 0 + + TP.BackButton { + id: backButton + Layout.leftMargin: leftHandleAnchor.visible ? 0 : Kirigami.Units.smallSpacing + Layout.minimumWidth: implicitHeight + Layout.minimumHeight: implicitHeight + Layout.maximumHeight: buttonsLayout.height + } + TP.ForwardButton { + id: forwardButton + Layout.minimumWidth: implicitHeight + Layout.minimumHeight: implicitHeight + Layout.maximumHeight: buttonsLayout.height + } + } + + QQC2.ToolSeparator { + visible: (menuButton.visible || (buttonsLayout.visible && buttonsLayout.visibleChildren.length > 0)) && breadcrumbVisible && pageRow.depth > 1 + } + + Loader { + id: breadcrumbLoader + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: -1 + Layout.preferredHeight: -1 + property Kirigami.PageRow pageRow: root + + asynchronous: true + + active: globalToolBar.actualStyle === Kirigami.ApplicationHeaderStyle.Breadcrumb + && header.currentItem + && header.currentItem.globalToolBarStyle !== Kirigami.ApplicationHeaderStyle.None + + source: Qt.resolvedUrl("BreadcrumbControl.qml") + } + + Item { + id: rightHandleAnchor + visible: header.__shouldHandleAnchorBeVisible(rightHandleAnchor, "contextDrawer", "trailingVisibleItem") + + Layout.preferredHeight: Math.max(backButton.implicitHeight, parent.height) + Layout.preferredWidth: height + } + } + background.opacity: breadcrumbLoader.active ? 1 : 0 +} diff --git a/src/controls/private/globaltoolbar/TitlesPageHeader.qml b/src/controls/private/globaltoolbar/TitlesPageHeader.qml new file mode 100644 index 0000000..c14d780 --- /dev/null +++ b/src/controls/private/globaltoolbar/TitlesPageHeader.qml @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts + +AbstractPageHeader { + id: root + + Loader { + id: titleLoader + + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + right: parent.right + } + height: Math.min(root.height, item + ? (item.Layout.preferredHeight > 0 ? item.Layout.preferredHeight : item.implicitHeight) + : 0) + + // Don't load async to prevent jumpy behaviour on slower devices as it loads in. + // If the title delegate really needs to load async, it should be its responsibility to do it itself. + asynchronous: false + sourceComponent: page ? page.titleDelegate : null + } +} diff --git a/src/controls/private/globaltoolbar/ToolBarPageFooter.qml b/src/controls/private/globaltoolbar/ToolBarPageFooter.qml new file mode 100644 index 0000000..bee6f39 --- /dev/null +++ b/src/controls/private/globaltoolbar/ToolBarPageFooter.qml @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2023 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +QQC2.ToolBar { + id: root + position: QQC2.ToolBar.Footer + + NumberAnimation { + id: appearAnim + target: root + property: "height" + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + + Connections { + target: applicationWindow() + function onControlsVisibleChanged() { + if (applicationWindow().controlsVisible) { + appearAnim.from = 0; + appearAnim.to = root.implicitHeight; + } else { + appearAnim.from = root.implicitHeight; + appearAnim.to = 0; + } + appearAnim.restart(); + } + } + + contentItem: Kirigami.ActionToolBar { + display: QQC2.Button.TextUnderIcon + alignment: Qt.AlignCenter + actions: root.parent.page.actions + } +} diff --git a/src/controls/private/globaltoolbar/ToolBarPageHeader.qml b/src/controls/private/globaltoolbar/ToolBarPageHeader.qml new file mode 100644 index 0000000..e5d20bf --- /dev/null +++ b/src/controls/private/globaltoolbar/ToolBarPageHeader.qml @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQml +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +AbstractPageHeader { + id: root + + implicitWidth: layout.implicitWidth + Kirigami.Units.smallSpacing * 2 + implicitHeight: Math.max(titleLoader.implicitHeight, toolBar.implicitHeight) + Kirigami.Units.smallSpacing * 2 + + onActiveFocusChanged: if (activeFocus && toolBar.actions.length > 0) { + toolBar.contentItem.visibleChildren[0].forceActiveFocus(Qt.TabFocusReason) + } + + MouseArea { + anchors.fill: parent + onPressed: mouse => { + page.forceActiveFocus() + mouse.accepted = false + } + } + + RowLayout { + id: layout + anchors.fill: parent + anchors.rightMargin: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing + + Loader { + id: titleLoader + + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillWidth: item?.Layout.fillWidth ?? false + Layout.minimumWidth: item?.Layout.minimumWidth ?? -1 + Layout.preferredWidth: item?.Layout.preferredWidth ?? -1 + Layout.maximumWidth: item?.Layout.maximumWidth ?? -1 + + // Don't load async to prevent jumpy behaviour on slower devices as it loads in. + // If the title delegate really needs to load async, it should be its responsibility to do it itself. + asynchronous: false + sourceComponent: page?.titleDelegate ?? null + } + + Kirigami.ActionToolBar { + id: toolBar + + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + Layout.fillHeight: true + + visible: actions.length > 0 + alignment: pageRow?.globalToolBar.toolbarActionAlignment ?? Qt.AlignRight + heightMode: pageRow?.globalToolBar.toolbarActionHeightMode ?? Kirigami.ToolBarLayout.ConstrainIfLarger + + actions: page?.actions ?? [] + } + } +} diff --git a/src/controls/templates/AbstractApplicationHeader.qml b/src/controls/templates/AbstractApplicationHeader.qml new file mode 100644 index 0000000..88f6f73 --- /dev/null +++ b/src/controls/templates/AbstractApplicationHeader.qml @@ -0,0 +1,201 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +/** + * @brief An item that can be used as a title for the application. + * + * Scrolling the main page will make it taller or shorter (through the point of going away) + * It's a behavior similar to the typical mobile web browser addressbar + * the minimum, preferred and maximum heights of the item can be controlled with + * * minimumHeight: default is 0, i.e. hidden + * * preferredHeight: default is Units.gridUnit * 1.6 + * * maximumHeight: default is Units.gridUnit * 3 + * + * To achieve a titlebar that stays completely fixed just set the 3 sizes as the same + * + * @inherit QtQuick.Item + */ +Item { + id: root + z: 90 + property int minimumHeight: 0 + // Use an inline arrow function, referring to an external normal function makes QV4 crash, see https://bugreports.qt.io/browse/QTBUG-119395 + property int preferredHeight: mainItem.children.reduce((accumulator, item) => { + return Math.max(accumulator, item.implicitHeight); + }, 0) + topPadding + bottomPadding + property int maximumHeight: Kirigami.Units.gridUnit * 3 + + property int position: QQC2.ToolBar.Header + + property Kirigami.PageRow pageRow: __appWindow?.pageStack ?? null + property Kirigami.Page page: pageRow?.currentItem as Kirigami.Page ?? null + + default property alias contentItem: mainItem.data + readonly property int paintedHeight: headerItem.y + headerItem.height - 1 + + property int leftPadding: 0 + property int topPadding: 0 + property int rightPadding: 0 + property int bottomPadding: 0 + property bool separatorVisible: true + + /** + * This property specifies whether the header should be pushed back when + * scrolling using the touch screen. + */ + property bool hideWhenTouchScrolling: root.pageRow?.globalToolBar.hideWhenTouchScrolling ?? false + + LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft + LayoutMirroring.childrenInherit: true + + Kirigami.Theme.inherit: true + + // FIXME: remove + property QtObject __appWindow: typeof applicationWindow !== "undefined" ? applicationWindow() : null + implicitHeight: preferredHeight + height: Layout.preferredHeight + + /** + * @brief This property holds the background item. + * @note the background will be automatically sized to fill the whole control + */ + property Item background + + onBackgroundChanged: { + background.z = -1; + background.parent = headerItem; + background.anchors.fill = headerItem; + } + + Component.onCompleted: AppHeaderSizeGroup.items.push(this) + + onMinimumHeightChanged: implicitHeight = preferredHeight; + onPreferredHeightChanged: implicitHeight = preferredHeight; + + opacity: height > 0 ? 1 : 0 + + NumberAnimation { + id: heightAnim + target: root + property: "implicitHeight" + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + + Connections { + target: root.__appWindow + + function onControlsVisibleChanged() { + heightAnim.from = root.implicitHeight; + heightAnim.to = root.__appWindow.controlsVisible ? root.preferredHeight : 0; + heightAnim.restart(); + } + } + + Connections { + target: root.page?.Kirigami.ColumnView ?? null + + function onScrollIntention(event) { + headerItem.scrollIntentHandler(event); + } + } + + Item { + id: headerItem + anchors { + left: parent.left + right: parent.right + bottom: !Kirigami.Settings.isMobile || root.position === QQC2.ToolBar.Header ? parent.bottom : undefined + top: !Kirigami.Settings.isMobile || root.position === QQC2.ToolBar.Footer ? parent.top : undefined + } + + height: Math.max(root.height, root.minimumHeight > 0 ? root.minimumHeight : root.preferredHeight) + + function scrollIntentHandler(event) { + if (!root.hideWhenTouchScrolling) { + return + } + + if (root.pageRow + && root.pageRow.globalToolBar.actualStyle !== Kirigami.ApplicationHeaderStyle.Breadcrumb) { + return; + } + if (!root.page.flickable || (root.page.flickable.atYBeginning && root.page.flickable.atYEnd)) { + return; + } + + root.implicitHeight = Math.max(0, Math.min(root.preferredHeight, root.implicitHeight + event.delta.y)) + event.accepted = root.implicitHeight > 0 && root.implicitHeight < root.preferredHeight; + slideResetTimer.restart(); + if ((root.page.flickable instanceof ListView) && root.page.flickable.verticalLayoutDirection === ListView.BottomToTop) { + root.page.flickable.contentY -= event.delta.y; + } + } + + Connections { + target: root.page?.globalToolBarItem ?? null + enabled: target + function onImplicitHeightChanged() { + root.implicitHeight = root.page.globalToolBarItem.implicitHeight; + } + } + + Timer { + id: slideResetTimer + interval: 500 + onTriggered: { + if ((root.pageRow?.wideMode ?? (root.__appWindow?.wideScreen ?? false)) || !Kirigami.Settings.isMobile) { + return; + } + if (root.height > root.minimumHeight + (root.preferredHeight - root.minimumHeight) / 2) { + heightAnim.to = root.preferredHeight; + } else { + heightAnim.to = root.minimumHeight; + } + heightAnim.from = root.implicitHeight + heightAnim.restart(); + } + } + + Connections { + target: pageRow + function onCurrentItemChanged() { + if (!root.page) { + return; + } + + heightAnim.from = root.implicitHeight; + heightAnim.to = root.preferredHeight; + + heightAnim.restart(); + } + } + + Item { + id: mainItem + clip: childrenRect.width > width + + onChildrenChanged: { + for (const child of children) { + child.anchors.verticalCenter = verticalCenter; + } + } + + anchors { + fill: parent + topMargin: root.topPadding + leftMargin: root.leftPadding + rightMargin: root.rightPadding + bottomMargin: root.bottomPadding + } + } + } +} diff --git a/src/controls/templates/AbstractCard.qml b/src/controls/templates/AbstractCard.qml new file mode 100644 index 0000000..5ccd6b3 --- /dev/null +++ b/src/controls/templates/AbstractCard.qml @@ -0,0 +1,128 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami + +/** + * A AbstractCard is the base for cards. A Card is a visual object that serves + * as an entry point for more detailed information. An abstractCard is empty, + * providing just the look and the base properties and signals for an ItemDelegate. + * It can be filled with any custom layout of items, its content is organized + * in 3 properties: header, contentItem and footer. + * Use this only when you need particular custom contents, for a standard layout + * for cards, use the Card component. + * + * @see Card + * @inherit QtQuick.Controls.ItemDelegate + * @since 2.4 + */ +T.ItemDelegate { + id: root + +//BEGIN properties + /** + * @brief This property holds an item that serves as a header. + * + * This item will be positioned on top if headerOrientation is ``Qt.Vertical`` + * or on the left if it is ``Qt.Horizontal``. + */ + property alias header: headerFooterLayout.header + + /** + * @brief This property sets the card's orientation. + * + * * ``Qt.Vertical``: the header will be positioned on top + * * ``Qt.Horizontal``: the header will be positioned on the left (or right if an RTL layout is used) + * + * default: ``Qt.Vertical`` + * + * @property Qt::Orientation headerOrientation + */ + property int headerOrientation: Qt.Vertical + + /** + * @brief This property holds an item that serves as a footer. + * + * This item will be positioned at the bottom if headerOrientation is ``Qt.Vertical`` + * or on the right if it is ``Qt.Horizontal``. + */ + property alias footer: headerFooterLayout.footer + + /** + * @brief This property sets whether clicking or tapping on the card area shows a visual click feedback. + * + * Use this if you want to do an action in the onClicked signal handler of the card. + * + * default: ``false`` + */ + property bool showClickFeedback: false + +//END properties + + Layout.fillWidth: true + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + outerPaddingLayout.implicitWidth) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + outerPaddingLayout.implicitHeight) + + hoverEnabled: !Kirigami.Settings.tabletMode && showClickFeedback + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.View + + width: ListView.view ? ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin : undefined + padding: Kirigami.Units.largeSpacing + + // Card component repurposes control's contentItem property, so it has to + // reimplement content layout and its padding manually. + Kirigami.Padding { + id: outerPaddingLayout + + anchors.fill: parent + + topPadding: root.topPadding + leftPadding: root.leftPadding + rightPadding: root.rightPadding + bottomPadding: root.bottomPadding + + contentItem: Kirigami.HeaderFooterLayout { + id: headerFooterLayout + + contentItem: Kirigami.Padding { + id: innerPaddingLayout + + contentItem: root.contentItem + + // Hide it altogether, so that vertical padding won't be + // included in control's total implicit height. + visible: contentItem !== null + + topPadding: headerFooterLayout.header ? Kirigami.Units.largeSpacing : 0 + bottomPadding: headerFooterLayout.footer ? Kirigami.Units.largeSpacing : 0 + } + } + } + + // HACK: A Control like this ItemDelegate tries to manage its + // contentItem's positioning, so we need to override that. This is + // equivalent to declaring x/y/width/height bindings in QQC2 style + // implementations. + Connections { + target: root.contentItem + + function onXChanged() { + root.contentItem.x = 0; + } + + function onYChanged() { + root.contentItem.y = Qt.binding(() => innerPaddingLayout.topPadding); + } + } +} diff --git a/src/controls/templates/Chip.qml b/src/controls/templates/Chip.qml new file mode 100644 index 0000000..59716f7 --- /dev/null +++ b/src/controls/templates/Chip.qml @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2022 Felipe Kinoshita +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Templates as T + +/** + * @brief Chip is a visual object based on AbstractButton + * that provides a friendly way to display predetermined elements + * with the visual styling of "tags" or "tokens." + * + * @see Chip + * @since 2.19 + * @inherit QtQuick.Controls.AbstractButton + */ +T.AbstractButton { + id: chip + + /** + * @brief This property holds whether or not to display a close button. + * + * default: ``true`` + */ + property bool closable: true + + /** + * @brief This property holds whether the icon should be masked or not. This controls the Kirigami.Icon.isMask property. + * + * default: ``false`` + */ + property bool iconMask: false + + /** + * @brief This signal is emitted when the close button has been clicked. + */ + signal removed() +} diff --git a/src/controls/templates/InlineMessage.qml b/src/controls/templates/InlineMessage.qml new file mode 100644 index 0000000..6781c43 --- /dev/null +++ b/src/controls/templates/InlineMessage.qml @@ -0,0 +1,411 @@ +/* + * SPDX-FileCopyrightText: 2018 Eike Hein + * SPDX-FileCopyrightText: 2022 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami +import org.kde.kirigami.templates.private as TP + +/** + * An inline message item with support for informational, positive, + * warning and error types, and with support for associated actions. + * + * InlineMessage can be used to give information to the user or + * interact with the user, without requiring the use of a dialog. + * + * The InlineMessage item is hidden by default. It also manages its + * height (and implicitHeight) during an animated reveal when shown. + * You should avoid setting height on an InlineMessage unless it is + * already visible. + * + * Optionally an icon can be set, defaulting to an icon appropriate + * to the message type otherwise. + * + * Optionally a close button can be shown. + * + * Actions are added from left to right. If more actions are set than + * can fit, an overflow menu is provided. + * + * Example: + * @code + * import org.kde.kirigami as Kirigami + * + * Kirigami.InlineMessage { + * type: Kirigami.MessageType.Error + * + * text: i18n("My error message") + * + * actions: [ + * Kirigami.Action { + * icon.name: "list-add" + * text: i18n("Add") + * onTriggered: source => { + * // do stuff + * } + * }, + * Kirigami.Action { + * icon.name: "edit" + * text: i18n("Edit") + * onTriggered: source => { + * // do stuff + * } + * } + * ] + * } + * @endcode + * + * @since 5.45 + * @inherit QtQuick.Templates.Control + */ +T.Control { + id: root + + visible: false + + /** + * Defines a position for the message: whether it's to be used as an inline component inside the page, + * a page header, or a page footer. + */ + enum Position { + Inline, + Header, + Footer + } + + /** + * Adjust the look of the message based upon the position. + * If a message is positioned in the header area or in the footer area + * of a page, it might be desirable to not have borders but just a line + * separating it from the content area. In this case, use the Header or + * Footer position. + * Default is InlineMessage.Position.Inline + */ + property int position: InlineMessage.Position.Inline + + /** + * This signal is emitted when a link is hovered in the message text. + * @param The hovered link. + */ + signal linkHovered(string link) + + /** + * This signal is emitted when a link is clicked or tapped in the message text. + * @param The clicked or tapped link. + */ + signal linkActivated(string link) + + /** + * This property holds the link embedded in the message text that the user is hovering over. + */ + readonly property alias hoveredLink: label.hoveredLink + + /** + * This property holds the message type. One of Information, Positive, Warning or Error. + * + * The default is Kirigami.MessageType.Information. + */ + property int type: Kirigami.MessageType.Information + + /** + * This grouped property holds the description of an optional icon. + * + * * source: The source of the icon, a freedesktop-compatible icon name is recommended. + * * color: An optional tint color for the icon. + * + * If no custom icon is set, an icon appropriate to the message type + * is shown. + */ + property TP.IconPropertiesGroup icon: TP.IconPropertiesGroup {} + + /** + * This property holds the message text. + */ + property string text + + /** + * This property holds whether the close button is displayed. + * + * The default is false. + */ + property bool showCloseButton: false + + /** + * This property holds the list of actions to show. Actions are added from left to + * right. If more actions are set than can fit, an overflow menu is + * provided. + */ + property list actions + + /** + * This property holds whether the current message item is animating. + */ + readonly property bool animating: _animating + + property bool _animating: false + + implicitHeight: visible ? (contentLayout.implicitHeight + topPadding + bottomPadding) : 0 + + padding: Kirigami.Units.smallSpacing + + Accessible.role: Accessible.AlertMessage + Accessible.ignored: !visible + + Behavior on implicitHeight { + enabled: !root.visible + + SequentialAnimation { + PropertyAction { targets: root; property: "_animating"; value: true } + NumberAnimation { duration: Kirigami.Units.longDuration } + } + } + + onVisibleChanged: { + if (!visible) { + contentLayout.opacity = 0; + } + } + + opacity: visible ? 1 : 0 + + Behavior on opacity { + enabled: !root.visible + + NumberAnimation { duration: Kirigami.Units.shortDuration } + } + + onOpacityChanged: { + if (opacity === 0) { + contentLayout.opacity = 0; + } else if (opacity === 1) { + contentLayout.opacity = 1; + } + } + + onImplicitHeightChanged: { + height = implicitHeight; + } + + contentItem: Item { + id: contentLayout + + // Used to defer opacity animation until we know if InlineMessage was + // initialized visible. + property bool complete: false + + Behavior on opacity { + enabled: root.visible && contentLayout.complete + + SequentialAnimation { + NumberAnimation { duration: Kirigami.Units.shortDuration * 2 } + PropertyAction { targets: root; property: "_animating"; value: false } + } + } + + implicitHeight: { + if (atBottom) { + return label.implicitHeight + actionsLayout.implicitHeight + actionsLayout.anchors.topMargin + } else { + return Math.max(icon.implicitHeight, label.implicitHeight, closeButton.implicitHeight, actionsLayout.implicitHeight) + } + } + + Accessible.ignored: true + + readonly property real remainingWidth: width - ( + icon.width + + label.anchors.leftMargin + label.implicitWidth + label.anchors.rightMargin + + (root.showCloseButton ? closeButton.width : 0) + ) + readonly property bool multiline: remainingWidth <= 0 || atBottom + + readonly property bool atBottom: (root.actions.length > 0) && (label.lineCount > 1 || actionsLayout.implicitWidth > remainingWidth) + + Kirigami.Icon { + id: icon + + width: Kirigami.Units.iconSizes.smallMedium + height: Kirigami.Units.iconSizes.smallMedium + + anchors { + left: parent.left + leftMargin: Kirigami.Units.smallSpacing + topMargin: Kirigami.Units.smallSpacing + } + + states: [ + State { + name: "multi-line" + when: contentLayout.atBottom || label.height > icon.height * 1.7 + AnchorChanges { + target: icon + anchors.top: icon.parent.top + anchors.verticalCenter: undefined + } + }, + // States are evaluated in the order they are declared. + // This is a fallback state. + State { + name: "single-line" + when: true + AnchorChanges { + target: icon + anchors.top: undefined + anchors.verticalCenter: parent.verticalCenter + } + } + ] + + source: { + if (root.icon.name) { + return root.icon.name; + } else if (root.icon.source) { + return root.icon.source; + } + + switch (root.type) { + case Kirigami.MessageType.Positive: + return "emblem-success"; + case Kirigami.MessageType.Warning: + return "emblem-warning"; + case Kirigami.MessageType.Error: + return "emblem-error"; + default: + return "emblem-information"; + } + } + + color: root.icon.color + + Accessible.ignored: !root.visible + Accessible.name: { + switch (root.type) { + case Kirigami.MessageType.Positive: + return qsTr("Success"); + case Kirigami.MessageType.Warning: + return qsTr("Warning"); + case Kirigami.MessageType.Error: + return qsTr("Error"); + default: + return qsTr("Note"); + } + } + } + + Kirigami.SelectableLabel { + id: label + + anchors { + left: icon.right + leftMargin: Kirigami.Units.largeSpacing + right: root.showCloseButton ? closeButton.left : parent.right + rightMargin: root.showCloseButton ? Kirigami.Units.smallSpacing : 0 + top: parent.top + } + + color: Kirigami.Theme.textColor + wrapMode: Text.WordWrap + + text: root.text + + verticalAlignment: Text.AlignVCenter + + // QTBUG-117667 TextEdit (super-type of SelectableLabel) needs + // very specific state-management trick so it doesn't get stuck. + // State names serve purely as a description. + states: [ + State { + name: "multi-line" + when: contentLayout.multiline + AnchorChanges { + target: label + anchors.bottom: undefined + } + PropertyChanges { + target: label + height: label.implicitHeight + } + }, + // States are evaluated in the order they are declared. + // This is a fallback state. + State { + name: "single-line" + when: true + AnchorChanges { + target: label + anchors.bottom: label.parent.bottom + } + } + ] + + onLinkHovered: link => root.linkHovered(link) + onLinkActivated: link => root.linkActivated(link) + + Accessible.ignored: !root.visible + } + + Kirigami.ActionToolBar { + id: actionsLayout + + flat: false + actions: root.actions + visible: root.actions.length > 0 + Accessible.ignored: !visible || !root.visible + alignment: Qt.AlignRight + + anchors { + left: parent.left + top: contentLayout.atBottom ? label.bottom : parent.top + topMargin: contentLayout.atBottom ? Kirigami.Units.largeSpacing : 0 + right: (!contentLayout.atBottom && root.showCloseButton) ? closeButton.left : parent.right + rightMargin: !contentLayout.atBottom && root.showCloseButton ? Kirigami.Units.smallSpacing : 0 + } + } + + QQC2.ToolButton { + id: closeButton + + visible: root.showCloseButton + + anchors.right: parent.right + + // Incompatible anchors need to be evaluated in a given order, + // which simple declarative bindings cannot assure + states: [ + State { + name: "onTop" + when: contentLayout.atBottom + AnchorChanges { + target: closeButton + anchors.top: parent.top + anchors.verticalCenter: undefined + } + } , + State { + name: "centered" + AnchorChanges { + target: closeButton + anchors.top: undefined + anchors.verticalCenter: parent.verticalCenter + } + } + ] + + height: contentLayout.atBottom ? implicitHeight : implicitHeight + + text: qsTr("Close") + display: QQC2.ToolButton.IconOnly + icon.name: "dialog-close" + + onClicked: root.visible = false + + Accessible.ignored: !root.visible + } + + Component.onCompleted: complete = true + } +} diff --git a/src/controls/templates/OverlayDrawer.qml b/src/controls/templates/OverlayDrawer.qml new file mode 100644 index 0000000..6867e60 --- /dev/null +++ b/src/controls/templates/OverlayDrawer.qml @@ -0,0 +1,385 @@ +/* + * SPDX-FileCopyrightText: 2012 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import "private" as KTP + +/** + * Overlay Drawers are used to expose additional UI elements needed for + * small secondary tasks for which the main UI elements are not needed. + * For example in Okular Mobile, an Overlay Drawer is used to display + * thumbnails of all pages within a document along with a search field. + * This is used for the distinct task of navigating to another page. + * + * @inherit QtQuick.Controls.Drawer + */ +T.Drawer { + id: root + +//BEGIN properties + /** + * @brief This property tells whether the drawer is open and visible. + * + * default: ``false`` + */ + property bool drawerOpen: false + + /** + * @brief This property tells whether the drawer is in a state between open + * and closed. + * + * The drawer is visible but not completely open. This is usually the case when + * the user is dragging the drawer from a screen edge, so the user is "peeking" + * at what's in the drawer. + * + * default: ``false`` + */ + property bool peeking: false + + /** + * @brief This property tells whether the drawer is currently opening or closing itself. + */ + readonly property bool animating : enterAnimation.animating || exitAnimation.animating || positionResetAnim.running + + /** + * @brief This property holds whether the drawer can be collapsed to a + * very thin, usually icon only sidebar. + * + * Only modal drawers are collapsible. Collapsible is not supported in + * the mobile mode. + * + * @since 2.5 + */ + property bool collapsible: false + + /** + * @brief This property tells whether the drawer is collapsed to a + * very thin sidebar, usually icon only. + * + * When true, the drawer will be collapsed to a very thin sidebar, + * usually icon only. + * + * default: ``false`` + * + * @see collapsible Only collapsible drawers can be collapsed. + */ + property bool collapsed: false + + /** + * @brief This property holds the size of the collapsed drawer. + * + * For vertical drawers this will be the width of the drawer and for horizontal + * drawers this will be the height of the drawer. + * + * default: ``Units.iconSizes.medium``, just enough to accommodate medium sized icons + */ + property int collapsedSize: Kirigami.Units.iconSizes.medium + + /** + * @brief This property holds the options for handle's open icon. + * + * This is a grouped property with following components: + * + * * ``source: var``: The name of a freedesktop-compatible icon. + * * ``color: color``: An optional tint color for the icon. + * + * If no custom icon is set, a menu icon is shown for the application globalDrawer + * and an overflow menu icon is shown for the contextDrawer. + * That's the default for the GlobalDrawer and ContextDrawer components respectively. + * + * For OverlayDrawer the default is view-right-close or view-left-close depending on + * the drawer location + * + * @since 2.5 + */ + readonly property KTP.IconPropertiesGroup handleOpenIcon: KTP.IconPropertiesGroup { + source: root.edge === Qt.RightEdge ? "view-right-close" : "view-left-close" + } + + /** + * @brief This property holds the options for the handle's close icon. + * + * This is a grouped property with the following components: + * * ``source: var``: The name of a freedesktop-compatible icon. + * * ``color: color``: An optional tint color for the icon. + * + * If no custom icon is set, an X icon is shown, + * which will morph into the Menu or overflow icons. + * + * For OverlayDrawer the default is view-right-new or view-left-new depending on + * the drawer location. + * + * @since 2.5 + */ + property KTP.IconPropertiesGroup handleClosedIcon: KTP.IconPropertiesGroup { + source: root.edge === Qt.RightEdge ? "view-right-new" : "view-left-new" + } + + /** + * @brief This property holds the tooltip displayed when the drawer is open. + * @since 2.15 + */ + property string handleOpenToolTip: qsTr("Close drawer") + + /** + * @brief This property holds the tooltip displayed when the drawer is closed. + * @since 2.15 + */ + property string handleClosedToolTip: qsTr("Open drawer") + + /** + * @brief This property holds whether the handle is visible, to make opening the + * drawer easier. + * + * Currently supported only on left and right drawers. + */ + property bool handleVisible: { + if (typeof applicationWindow === "function") { + const w = applicationWindow(); + if (w) { + return w.controlsVisible; + } + } + // For a generic-purpose OverlayDrawer its handle is visible by default + return true; + } + + /** + * @brief Readonly property that points to the item that will act as a physical + * handle for the Drawer. + * @property MouseArea handle + **/ + readonly property Item handle: KTP.DrawerHandle { + drawer: root + } +//END properties + + interactive: modal + + z: Kirigami.OverlayZStacking.z + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: modal ? Kirigami.Theme.View : Kirigami.Theme.Window + Kirigami.Theme.onColorSetChanged: { + contentItem.Kirigami.Theme.colorSet = Kirigami.Theme.colorSet + background.Kirigami.Theme.colorSet = Kirigami.Theme.colorSet + } + +//BEGIN reassign properties + //default paddings + leftPadding: Kirigami.Units.smallSpacing + topPadding: Kirigami.Units.smallSpacing + rightPadding: Kirigami.Units.smallSpacing + bottomPadding: Kirigami.Units.smallSpacing + + y: modal ? 0 : ((T.ApplicationWindow.menuBar && T.ApplicationWindow.menuBar.visible ? T.ApplicationWindow.menuBar.height : 0) + (T.ApplicationWindow.header && T.ApplicationWindow.header.visible ? T.ApplicationWindow.header.height : 0)) + + height: parent && (root.edge === Qt.LeftEdge || root.edge === Qt.RightEdge) ? (modal ? parent.height : (parent.height - y - (T.ApplicationWindow.footer ? T.ApplicationWindow.footer.height : 0))) : implicitHeight + + parent: modal || edge === Qt.LeftEdge || edge === Qt.RightEdge ? T.ApplicationWindow.overlay : T.ApplicationWindow.contentItem + + edge: Qt.LeftEdge + modal: true + dim: modal + QQC2.Overlay.modal: Rectangle { + color: Qt.rgba(0, 0, 0, 0.35) + } + + dragMargin: enabled && (edge === Qt.LeftEdge || edge === Qt.RightEdge) ? Math.min(Kirigami.Units.gridUnit, Qt.styleHints.startDragDistance) : 0 + + contentWidth: contentItem.implicitWidth || (contentChildren.length === 1 ? contentChildren[0].implicitWidth : 0) + contentHeight: contentItem.implicitHeight || (contentChildren.length === 1 ? contentChildren[0].implicitHeight : 0) + + implicitWidth: contentWidth + leftPadding + rightPadding + implicitHeight: contentHeight + topPadding + bottomPadding + + enter: Transition { + SequentialAnimation { + id: enterAnimation + /* NOTE: why this? the running status of the enter transition is not relaible and + * the SmoothedAnimation is always marked as non running, + * so the only way to get to a reliable animating status is with this + */ + property bool animating + ScriptAction { + script: { + enterAnimation.animating = true + // on non modal dialog we don't want drawers in the overlay + if (!root.modal) { + root.background.parent.parent = applicationWindow().overlay.parent + } + } + } + SmoothedAnimation { + velocity: 5 + } + ScriptAction { + script: enterAnimation.animating = false + } + } + } + + exit: Transition { + SequentialAnimation { + id: exitAnimation + property bool animating + ScriptAction { + script: exitAnimation.animating = true + } + SmoothedAnimation { + velocity: 5 + } + ScriptAction { + script: exitAnimation.animating = false + } + } + } +//END reassign properties + + +//BEGIN signal handlers + onCollapsedChanged: { + if (Kirigami.Settings.isMobile) { + collapsed = false; + } + if (!__internal.completed) { + return; + } + if ((!collapsible || modal) && collapsed) { + collapsed = true; + } + } + onCollapsibleChanged: { + if (Kirigami.Settings.isMobile) { + collapsible = false; + } + if (!__internal.completed) { + return; + } + if (!collapsible) { + collapsed = false; + } else if (modal) { + collapsible = false; + } + } + onModalChanged: { + if (!__internal.completed) { + return; + } + if (modal) { + collapsible = false; + } + } + + onPositionChanged: { + if (peeking) { + visible = true + } + } + onVisibleChanged: { + if (peeking) { + visible = true + } else { + drawerOpen = visible; + } + } + onPeekingChanged: { + if (peeking) { + root.enter.enabled = false; + root.exit.enabled = false; + } else { + drawerOpen = position > 0.5 ? 1 : 0; + positionResetAnim.running = true + root.enter.enabled = true; + root.exit.enabled = true; + } + } + onDrawerOpenChanged: { + // sync this property only when the component is properly loaded + if (!__internal.completed) { + return; + } + positionResetAnim.running = false; + if (drawerOpen) { + open(); + } else { + close(); + } + Qt.callLater(() => root.handle.displayToolTip = true) + } + + Component.onCompleted: { + // if defined as drawerOpen by default in QML, don't animate + if (root.drawerOpen) { + root.enter.enabled = false; + root.visible = true; + root.position = 1; + root.enter.enabled = true; + } + __internal.completed = true; + contentItem.Kirigami.Theme.colorSet = Kirigami.Theme.colorSet; + background.Kirigami.Theme.colorSet = Kirigami.Theme.colorSet; + } +//END signal handlers + + // this is as hidden as it can get here + property QtObject __internal: QtObject { + //here in order to not be accessible from outside + property bool completed: false + property SequentialAnimation positionResetAnim: SequentialAnimation { + id: positionResetAnim + property alias to: internalAnim.to + NumberAnimation { + id: internalAnim + target: root + to: drawerOpen ? 1 : 0 + property: "position" + duration: (root.position)*Kirigami.Units.longDuration + } + ScriptAction { + script: { + root.drawerOpen = internalAnim.to !== 0; + } + } + } + readonly property Item statesItem: Item { + states: [ + State { + when: root.collapsed + PropertyChanges { + target: root + implicitWidth: edge === Qt.TopEdge || edge === Qt.BottomEdge ? applicationWindow().width : Math.min(collapsedSize + leftPadding + rightPadding, Math.round(applicationWindow().width*0.8)) + + implicitHeight: edge === Qt.LeftEdge || edge === Qt.RightEdge ? applicationWindow().height : Math.min(collapsedSize + topPadding + bottomPadding, Math.round(applicationWindow().height*0.8)) + } + }, + State { + when: !root.collapsed + PropertyChanges { + target: root + implicitWidth: edge === Qt.TopEdge || edge === Qt.BottomEdge ? applicationWindow().width : Math.min(contentItem.implicitWidth, Math.round(applicationWindow().width*0.8)) + + implicitHeight: edge === Qt.LeftEdge || edge === Qt.RightEdge ? applicationWindow().height : Math.min(contentHeight + topPadding + bottomPadding, Math.round(applicationWindow().height*0.4)) + + contentWidth: contentItem.implicitWidth || (contentChildren.length === 1 ? contentChildren[0].implicitWidth : 0) + contentHeight: contentItem.implicitHeight || (contentChildren.length === 1 ? contentChildren[0].implicitHeight : 0) + } + } + ] + transitions: Transition { + reversible: true + NumberAnimation { + properties: root.edge === Qt.TopEdge || root.edge === Qt.BottomEdge ? "implicitHeight" : "implicitWidth" + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + } +} diff --git a/src/controls/templates/OverlaySheet.qml b/src/controls/templates/OverlaySheet.qml new file mode 100644 index 0000000..c420b3d --- /dev/null +++ b/src/controls/templates/OverlaySheet.qml @@ -0,0 +1,437 @@ +/* + * SPDX-FileCopyrightText: 2016-2023 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami + +/** + * @brief An overlay sheet that covers the current Page content. + * + * Its contents can be scrolled up or down, scrolling all the way up or + * all the way down, dismisses it. + * Use this for big, modal dialogs or information display, that can't be + * logically done as a new separate Page, even if potentially + * are taller than the screen space. + * + * Example usage: + * @code + * Kirigami.OverlaySheet { + * ColumnLayout { ... } + * } + * Kirigami.OverlaySheet { + * ListView { ... } + * } + * @endcode + * + * It needs a single element declared inside, do *not* override its contentItem + * + * @inherit QtQuick.Templates.Popup + */ +T.Popup { + id: root + + Kirigami.OverlayZStacking.layer: Kirigami.OverlayZStacking.FullScreen + z: Kirigami.OverlayZStacking.z + + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + +//BEGIN Own Properties + + /** + * @brief A title to be displayed in the header of this Sheet + */ + property string title + + /** + * @brief This property sets the visibility of the close button in the top-right corner. + * + * default: `Only shown in desktop mode` + * + */ + property bool showCloseButton: !Kirigami.Settings.isMobile + + /** + * @brief This property holds an optional item which will be used as the sheet's header, + * and will always be displayed. + */ + property Item header: Kirigami.Heading { + level: 2 + text: root.title + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + + // use tooltip for long text that is elided + T.ToolTip.visible: truncated && titleHoverHandler.hovered + T.ToolTip.text: root.title + HoverHandler { + id: titleHoverHandler + } + } + + /** + * @brief An optional item which will be used as the sheet's footer, + * always kept on screen. + */ + property Item footer + + default property alias flickableContentData: scrollView.contentData +//END Own Properties + +//BEGIN Reimplemented Properties + QQC2.Overlay.modal: Rectangle { + color: Qt.rgba(0, 0, 0, 0.3) + + // the opacity of the item is changed internally by QQuickPopup on open/close + Behavior on opacity { + OpacityAnimator { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + + modal: true + dim: true + + leftInset: -1 + rightInset: -1 + topInset: -1 + bottomInset: -1 + + closePolicy: T.Popup.CloseOnEscape + x: parent ? Math.round(parent.width / 2 - width / 2) : 0 + y: { + if (!parent) { + return 0; + } + const visualParentAdjust = sheetHandler.visualParent?.y ?? 0; + const wantedPosition = parent.height / 2 - implicitHeight / 2; + return Math.round(Math.max(visualParentAdjust, wantedPosition, Kirigami.Units.gridUnit * 3)); + } + + width: root.parent ? Math.min(root.parent.width, implicitWidth) : implicitWidth + implicitWidth: { + let width = parent?.width ?? 0; + if (!scrollView.itemForSizeHints) { + return width; + } else if (scrollView.itemForSizeHints.Layout.preferredWidth > 0) { + return Math.min(width, scrollView.itemForSizeHints.Layout.preferredWidth); + } else if (scrollView.itemForSizeHints.implicitWidth > 0) { + return Math.min(width, scrollView.itemForSizeHints.implicitWidth); + } else { + return width; + } + } + implicitHeight: { + let h = parent?.height ?? 0; + if (!scrollView.itemForSizeHints) { + return h - y; + } else if (scrollView.itemForSizeHints.Layout.preferredHeight > 0) { + h = scrollView.itemForSizeHints.Layout.preferredHeight; + } else if (scrollView.itemForSizeHints.implicitHeight > 0) { + h = scrollView.itemForSizeHints.implicitHeight + Kirigami.Units.largeSpacing * 2; + } else if (scrollView.itemForSizeHints instanceof Flickable && scrollView.itemForSizeHints.contentHeight > 0) { + h = scrollView.itemForSizeHints.contentHeight + Kirigami.Units.largeSpacing * 2; + } else { + h = scrollView.itemForSizeHints.height; + } + h += headerItem.implicitHeight + footerParent.implicitHeight + topPadding + bottomPadding; + return parent ? Math.min(h, parent.height - y) : h + } +//END Reimplemented Properties + +//BEGIN Signal handlers + onVisibleChanged: { + const flickable = scrollView.contentItem; + flickable.contentY = flickable.originY - flickable.topMargin; + } + + Component.onCompleted: { + Qt.callLater(() => { + if (!root.parent && typeof applicationWindow !== "undefined") { + root.parent = applicationWindow().overlay + } + }); + } + + Connections { + target: parent + function onVisibleChanged() { + if (!parent.visible) { + root.close(); + } + } + } +//END Signal handlers + +//BEGIN UI + contentItem: MouseArea { + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + Kirigami.Theme.colorSet: root.Kirigami.Theme.colorSet + Kirigami.Theme.inherit: false + + property real scenePressY + property real lastY + property bool dragStarted + drag.filterChildren: true + DragHandler { + id: mouseDragBlocker + target: null + dragThreshold: 0 + acceptedDevices: PointerDevice.Mouse + onActiveChanged: { + if (active) { + parent.dragStarted = false; + } + } + } + + onPressed: mouse => { + scenePressY = mapToItem(null, mouse.x, mouse.y).y; + lastY = scenePressY; + dragStarted = false; + } + onPositionChanged: mouse => { + if (mouseDragBlocker.active) { + return; + } + const currentY = mapToItem(null, mouse.x, mouse.y).y; + + if (dragStarted && currentY !== lastY) { + translation.y += currentY - lastY; + } + if (Math.abs(currentY - scenePressY) > Qt.styleHints.startDragDistance) { + dragStarted = true; + } + lastY = currentY; + } + onCanceled: restoreAnim.restart(); + onReleased: mouse => { + if (mouseDragBlocker.active) { + return; + } + if (Math.abs(mapToItem(null, mouse.x, mouse.y).y - scenePressY) > Kirigami.Units.gridUnit * 5) { + root.close(); + } else { + restoreAnim.restart(); + } + } + + ColumnLayout { + id: mainLayout + anchors.fill: parent + spacing: 0 + + // Even though we're not actually using any shadows here, + // we're using a ShadowedRectangle instead of a regular + // rectangle because it allows fine-grained control over which + // corners to round, which we need here + Item { + id: headerItem + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + //Layout.margins: 1 + visible: root.header || root.showCloseButton + implicitHeight: Math.max(headerParent.implicitHeight, closeIcon.height)// + Kirigami.Units.smallSpacing * 2 + z: 2 + + Rectangle { + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + topMargin: Kirigami.Units.smallSpacing + } + width: Math.round(Kirigami.Units.gridUnit * 3) + height: Math.round(Kirigami.Units.gridUnit / 4) + radius: height + color: Kirigami.Theme.textColor + opacity: 0.4 + visible: Kirigami.Settings.hasTransientTouchInput + } + Kirigami.Padding { + id: headerParent + + readonly property real leadingPadding: Kirigami.Units.largeSpacing + readonly property real trailingPadding: (root.showCloseButton ? closeIcon.width : 0) + Kirigami.Units.smallSpacing + + anchors.fill: parent + verticalPadding: Kirigami.Units.largeSpacing + leftPadding: root.mirrored ? trailingPadding : leadingPadding + rightPadding: root.mirrored ? leadingPadding : trailingPadding + + contentItem: root.header + } + QQC2.ToolButton { + id: closeIcon + + // We want to position the close button in the top-right + // corner if the header is very tall, but we want to + // vertically center it in a short header + readonly property bool tallHeader: parent.height > (Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.largeSpacing * 2) + Layout.alignment: tallHeader ? Qt.AlignRight | Qt.AlignTop : Qt.AlignRight | Qt.AlignVCenter + Layout.topMargin: tallHeader ? Kirigami.Units.largeSpacing : 0 + anchors { + verticalCenter: !tallHeader ? undefined : parent.verticalCenter + right: parent.right + margins: Kirigami.Units.largeSpacing + } + z: 3 + + visible: root.showCloseButton + icon.name: closeIcon.hovered ? "window-close" : "window-close-symbolic" + text: qsTr("Close", "@action:button close dialog") + onClicked: root.close() + display: QQC2.AbstractButton.IconOnly + } + Kirigami.Separator { + anchors { + right: parent.right + left: parent.left + top: parent.bottom + } + visible: scrollView.T.ScrollBar.vertical.visible + } + } + + // Here goes the main Sheet content + QQC2.ScrollView { + id: scrollView + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + T.ScrollBar.horizontal.policy: T.ScrollBar.AlwaysOff + + property bool initialized: false + property Item itemForSizeHints + + // Important to not even access contentItem before it has been spontaneously created + contentWidth: initialized ? contentItem.width : width + contentHeight: itemForSizeHints?.implicitHeight ?? 0 + + onContentItemChanged: { + initialized = true; + const flickable = contentItem as Flickable; + flickable.boundsBehavior = Flickable.StopAtBounds; + if ((flickable instanceof ListView) || (flickable instanceof GridView)) { + itemForSizeHints = flickable; + return; + } + const content = flickable.contentItem; + content.childrenChanged.connect(() => { + for (const item of content.children) { + item.anchors.margins = Kirigami.Units.largeSpacing; + item.anchors.top = content.top; + item.anchors.left = content.left; + item.anchors.right = content.right; + } + itemForSizeHints = content.children?.[0] ?? null; + }); + } + } + + // Optional footer + Kirigami.Separator { + Layout.fillWidth: true + visible: footerParent.visible + } + Kirigami.Padding { + id: footerParent + Layout.fillWidth: true + padding: Kirigami.Units.smallSpacing + contentItem: root.footer + visible: contentItem !== null + } + } + Translate { + id: translation + } + MouseArea { + id: sheetHandler + readonly property Item visualParent: root.parent?.contentItem ?? root.parent + x: -root.x + y: -root.y + z: -1 + width: visualParent?.width ?? 0 + height: (visualParent?.height ?? 0) * 2 + + property var pressPos + onPressed: mouse => { + pressPos = mapToItem(null, mouse.x, mouse.y) + } + onReleased: mouse => { + // onClicked is emitted even if the mouse was dragged a lot, so we have to check the Manhattan length by hand + // https://en.wikipedia.org/wiki/Taxicab_geometry + let pos = mapToItem(null, mouse.x, mouse.y) + if (Math.abs(pos.x - pressPos.x) + Math.abs(pos.y - pressPos.y) < Qt.styleHints.startDragDistance) { + root.close(); + } + } + + NumberAnimation { + id: restoreAnim + target: translation + property: "y" + from: translation.y + to: 0 + easing.type: Easing.InOutQuad + duration: Kirigami.Units.longDuration + } + Component.onCompleted: { + root.contentItem.parent.transform = translation + root.contentItem.parent.clip = false + } + } + } +//END UI + +//BEGIN Transitions + enter: Transition { + ParallelAnimation { + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + easing.type: Easing.InOutQuad + duration: Kirigami.Units.longDuration + } + NumberAnimation { + target: translation + property: "y" + from: Kirigami.Units.gridUnit * 5 + to: 0 + easing.type: Easing.InOutQuad + duration: Kirigami.Units.longDuration + } + } + } + + exit: Transition { + ParallelAnimation { + NumberAnimation { + property: "opacity" + from: 1 + to: 0 + easing.type: Easing.InOutQuad + duration: Kirigami.Units.longDuration + } + NumberAnimation { + target: translation + property: "y" + from: translation.y + to: translation.y >= 0 ? translation.y + Kirigami.Units.gridUnit * 5 : translation.y - Kirigami.Units.gridUnit * 5 + easing.type: Easing.InOutQuad + duration: Kirigami.Units.longDuration + } + } + } +//END Transitions +} + diff --git a/src/controls/templates/SingletonHeaderSizeGroup.qml b/src/controls/templates/SingletonHeaderSizeGroup.qml new file mode 100644 index 0000000..93736b5 --- /dev/null +++ b/src/controls/templates/SingletonHeaderSizeGroup.qml @@ -0,0 +1,13 @@ +pragma Singleton + +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import org.kde.kirigami as Kirigami + +Kirigami.SizeGroup { + mode: Kirigami.SizeGroup.Height +} \ No newline at end of file diff --git a/src/controls/templates/private/BackButton.qml b/src/controls/templates/private/BackButton.qml new file mode 100644 index 0000000..9739b25 --- /dev/null +++ b/src/controls/templates/private/BackButton.qml @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +QQC2.ToolButton { + id: button + + icon.name: (LayoutMirroring.enabled ? "go-previous-symbolic-rtl" : "go-previous-symbolic") + + enabled: { + const pageStack = applicationWindow().pageStack; + + if (pageStack.layers.depth > 1) { + return true; + } + + if (pageStack.depth > 1) { + if (pageStack.currentIndex > 0) { + return true; + } + + const view = pageStack.columnView; + if (LayoutMirroring.enabled) { + return view.contentWidth - view.width < view.contentX + } else { + return view.contentX > 0; + } + } + + return false; + } + + // The gridUnit wiggle room is used to not flicker the button visibility during an animated resize for instance due to a sidebar collapse + visible: { + const pageStack = applicationWindow().pageStack; + const showNavButtons = globalToolBar?.showNavigationButtons ?? Kirigami.ApplicationHeaderStyle.NoNavigationButtons; + return pageStack.layers.depth > 1 || (pageStack.contentItem.contentWidth > pageStack.width + Kirigami.Units.gridUnit && (showNavButtons & Kirigami.ApplicationHeaderStyle.ShowBackButton)); + } + + onClicked: { + applicationWindow().pageStack.goBack(); + } + + text: qsTr("Navigate Back") + display: QQC2.ToolButton.IconOnly + + QQC2.ToolTip { + visible: button.hovered + text: button.text + delay: Kirigami.Units.toolTipDelay + timeout: 5000 + y: button.height + } +} diff --git a/src/controls/templates/private/BorderPropertiesGroup.qml b/src/controls/templates/private/BorderPropertiesGroup.qml new file mode 100644 index 0000000..3d36982 --- /dev/null +++ b/src/controls/templates/private/BorderPropertiesGroup.qml @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick + +QtObject { + /** + * @brief This property holds the color of this border. + */ + property color color + + /** + * @brief This property holds the width of this border. + */ + property real width +} diff --git a/src/controls/templates/private/ContextIcon.qml b/src/controls/templates/private/ContextIcon.qml new file mode 100644 index 0000000..1431cdc --- /dev/null +++ b/src/controls/templates/private/ContextIcon.qml @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +Item { + id: canvas + + property Kirigami.OverlayDrawer drawer + property color color: Kirigami.Theme.textColor + property int thickness: 2 + + property real position: drawer?.position ?? 0 + + width: Kirigami.Units.iconSizes.smallMedium + height: Kirigami.Units.iconSizes.smallMedium + opacity: 0.8 + layer.enabled: true + + LayoutMirroring.enabled: false + LayoutMirroring.childrenInherit: true + + Item { + id: iconRoot + anchors { + fill: parent + margins: Kirigami.Units.smallSpacing + } + Rectangle { + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + topMargin: (parent.height / 2 - canvas.thickness / 2) * canvas.position + } + antialiasing: canvas.position !== 0 + transformOrigin: Item.Center + width: (1 - canvas.position) * height + canvas.position * Math.sqrt(2 * parent.width * parent.width) + height: canvas.thickness + color: canvas.color + rotation: 45 * canvas.position + } + + Rectangle { + anchors.centerIn: parent + width: height + height: canvas.thickness + color: canvas.color + } + + Rectangle { + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: (parent.height / 2 - canvas.thickness / 2) * canvas.position + } + antialiasing: canvas.position !== 0 + transformOrigin: Item.Center + width: (1 - canvas.position) * height + canvas.position * Math.sqrt(2 * parent.width * parent.width) + height: canvas.thickness + color: canvas.color + rotation: -45 * canvas.position + } + } +} diff --git a/src/controls/templates/private/DrawerHandle.qml b/src/controls/templates/private/DrawerHandle.qml new file mode 100644 index 0000000..d977985 --- /dev/null +++ b/src/controls/templates/private/DrawerHandle.qml @@ -0,0 +1,208 @@ +/* + * SPDX-FileCopyrightText: 2023 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami + +MouseArea { + id: root + + /* + * This property is used to set when the tooltip is visible. + * It exists because the text is changed while the tooltip is still visible. + */ + property bool displayToolTip: true + + /** + * The drawer this handle will control + */ + // Should be KT.OverlayDrawer, but can't due to "Cyclic dependency" + property T.Drawer drawer + + readonly property T.Overlay overlay: drawer.T.Overlay.overlay + + // Above the Overlay when modal but below when non-modal and when the drawer is closed + // so that other overlays can be above it. + parent: overlay?.parent ?? null + z: overlay ? overlay.z + (drawer?.modal && drawer?.drawerOpen ? 1 : - 1) : 0 + + preventStealing: true + hoverEnabled: handleAnchor?.visible ?? false + + QQC2.ToolButton { + anchors.centerIn: parent + width: parent.height - Kirigami.Units.smallSpacing * 1.5 + height: parent.height - Kirigami.Units.smallSpacing * 1.5 + visible: !Kirigami.Settings.tabletMode && !Kirigami.Settings.hasTransientTouchInput + + Accessible.name: root.drawer.drawerOpen ? root.drawer.handleOpenToolTip : root.drawer.handleClosedToolTip + + onClicked: { + root.displayToolTip = false; + Qt.callLater(() => { + root.drawer.drawerOpen = !root.drawer.drawerOpen; + }) + } + Keys.onEscapePressed: { + if (root.drawer.closePolicy & T.Popup.CloseOnEscape) { + root.drawer.drawerOpen = false; + } + } + } + + QQC2.ToolTip.visible: displayToolTip && containsMouse + QQC2.ToolTip.text: drawer.drawerOpen ? handleOpenToolTip : handleClosedToolTip + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + + property Item handleAnchor: { + if (typeof applicationWindow === "undefined") { + return null; + } + const window = applicationWindow(); + const globalToolBar = window.pageStack?.globalToolBar; + if (!globalToolBar) { + return null; + } + return (drawer.edge === Qt.LeftEdge && !drawer.mirrored) || (drawer.edge === Qt.RightEdge && drawer.mirrored) + ? globalToolBar.leftHandleAnchor + : globalToolBar.rightHandleAnchor; + } + + property int startX + property int mappedStartX + + enabled: drawer.handleVisible + + onPressed: mouse => { + drawer.peeking = true; + startX = mouse.x; + mappedStartX = mapToItem(parent, startX, 0).x; + } + + onPositionChanged: mouse => { + if (!pressed) { + return; + } + const pos = mapToItem(parent, mouse.x - startX, mouse.y); + switch (drawer.edge) { + case Qt.LeftEdge: + drawer.position = pos.x / drawer.contentItem.width; + break; + case Qt.RightEdge: + drawer.position = (drawer.parent.width - pos.x - width) / drawer.contentItem.width; + break; + default: + } + } + + onReleased: mouse => { + drawer.peeking = false; + if (Math.abs(mapToItem(parent, mouse.x, 0).x - mappedStartX) < Qt.styleHints.startDragDistance) { + if (!drawer.drawerOpen) { + drawer.close(); + } + drawer.drawerOpen = !drawer.drawerOpen; + } + } + + onCanceled: { + drawer.peeking = false + } + + x: { + switch (drawer.edge) { + case Qt.LeftEdge: + return drawer.background.width * drawer.position + Kirigami.Units.smallSpacing; + case Qt.RightEdge: + return parent.width - (drawer.background.width * drawer.position) - width - Kirigami.Units.smallSpacing; + default: + return 0; + } + } + + Binding { + when: root.handleAnchor && root.anchors.bottom + target: root + property: "y" + value: root.handleAnchor ? root.handleAnchor.Kirigami.ScenePosition.y : 0 + restoreMode: Binding.RestoreBinding + } + + anchors { + bottom: handleAnchor ? undefined : parent.bottom + bottomMargin: { + if (typeof applicationWindow === "undefined") { + return undefined; + } + const window = applicationWindow(); + + let margin = Kirigami.Units.smallSpacing; + if (window.footer) { + margin = window.footer.height + Kirigami.Units.smallSpacing; + } + + if (drawer.parent && drawer.height < drawer.parent.height) { + margin = drawer.parent.height - drawer.height - drawer.y + Kirigami.Units.smallSpacing; + } + + if (!window || !window.pageStack || + !window.pageStack.contentItem || + !window.pageStack.contentItem.itemAt) { + return margin; + } + + let item; + if (window.pageStack.layers.depth > 1) { + item = window.pageStack.layers.currentItem; + } else { + item = window.pageStack.contentItem.itemAt(window.pageStack.contentItem.contentX + x, 0); + } + + // try to take the last item + if (!item) { + item = window.pageStack.lastItem; + } + + let pageFooter = item && item.page ? item.page.footer : (item ? item.footer : undefined); + if (pageFooter && drawer.parent) { + margin = drawer.height < drawer.parent.height ? margin : margin + pageFooter.height + } + + return margin; + } + + Behavior on bottomMargin { + NumberAnimation { + duration: Kirigami.Units.shortDuration + easing.type: Easing.InOutQuad + } + } + } + + visible: drawer.enabled && (drawer.edge === Qt.LeftEdge || drawer.edge === Qt.RightEdge) && opacity > 0 + width: handleAnchor?.visible ? handleAnchor.width : Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.smallSpacing * 2 + height: handleAnchor?.visible ? handleAnchor.height : width + opacity: drawer.handleVisible ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + + transform: Translate { + x: root.drawer.handleVisible ? 0 : (root.drawer.edge === Qt.LeftEdge ? -root.width : root.width) + Behavior on x { + NumberAnimation { + duration: Kirigami.Units.longDuration + easing.type: !root.drawer.handleVisible ? Easing.OutQuad : Easing.InQuad + } + } + } +} diff --git a/src/controls/templates/private/ForwardButton.qml b/src/controls/templates/private/ForwardButton.qml new file mode 100644 index 0000000..18e7075 --- /dev/null +++ b/src/controls/templates/private/ForwardButton.qml @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +QQC2.ToolButton { + id: button + + icon.name: (LayoutMirroring.enabled ? "go-next-symbolic-rtl" : "go-next-symbolic") + + enabled: applicationWindow().pageStack.currentIndex < applicationWindow().pageStack.depth-1 + + // The gridUnit wiggle room is used to not flicker the button visibility during an animated resize for instance due to a sidebar collapse + visible: { + const pageStack = applicationWindow().pageStack; + const showNavButtons = globalToolBar?.showNavigationButtons ?? Kirigami.ApplicationHeaderStyle.NoNavigationButtons; + return pageStack.layers.depth === 1 && pageStack.contentItem.contentWidth > pageStack.width + Kirigami.Units.gridUnit && (showNavButtons & Kirigami.ApplicationHeaderStyle.ShowForwardButton); + } + + onClicked: applicationWindow().pageStack.goForward(); + + text: qsTr("Navigate Forward") + display: QQC2.ToolButton.IconOnly + + QQC2.ToolTip { + visible: button.hovered + text: button.text + delay: Kirigami.Units.toolTipDelay + y: button.height + } +} diff --git a/src/controls/templates/private/GenericDrawerIcon.qml b/src/controls/templates/private/GenericDrawerIcon.qml new file mode 100644 index 0000000..ac9ad9b --- /dev/null +++ b/src/controls/templates/private/GenericDrawerIcon.qml @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +Item { + width: height + height: Kirigami.Units.iconSizes.smallMedium + property Kirigami.OverlayDrawer drawer + property color color: Kirigami.Theme.textColor + opacity: 0.8 + layer.enabled: true + + Kirigami.Icon { + selected: drawer.handle.pressed + opacity: 1 - drawer.position + anchors.fill: parent + source: drawer.handleClosedIcon.name ? drawer.handleClosedIcon.name : drawer.handleClosedIcon.source + color: drawer.handleClosedIcon.color + } + Kirigami.Icon { + selected: drawer.handle.pressed + opacity: drawer.position + anchors.fill: parent + source: drawer.handleOpenIcon.name ? drawer.handleOpenIcon.name : drawer.handleOpenIcon.source + color: drawer.handleOpenIcon.color + } +} + diff --git a/src/controls/templates/private/IconPropertiesGroup.qml b/src/controls/templates/private/IconPropertiesGroup.qml new file mode 100644 index 0000000..4539f03 --- /dev/null +++ b/src/controls/templates/private/IconPropertiesGroup.qml @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQml + +/** + * @brief Group of icon properties. + * + * This is a subset of those used in QQC2, Kirigami.Action still needs the full one as needs 100% api compatibility + */ +QtObject { + /** + * @brief This property holds icon name. + * + * The icon will be loaded from the platform theme. If the icon is found + * in the theme, it will always be used; even if icon.source is also set. + * If the icon is not found, icon.source will be used instead. + */ + property string name + + /** + * @brief This property holds the icon source. + * + * The icon will be loaded as a regular image. + * + * @see QtQuick.Image::source + */ + property var source + + /** + * @brief This property holds the icon tint color. + * + * The icon is tinted with the specified color, unless the color is set to "transparent". + */ + property color color: Qt.rgba(0, 0, 0, 0) + + /** + * This property holds the width of the icon. + */ + property real width + + /** + * This property holds the height of the icon. + */ + property real height + + /** + * Bind this icon to all matching properties of a Controls icon group. + * + * This function automatically binds all properties to matching properties + * of a controls icon group, since we cannot just reuse the Controls icon + * group. + * + * To use it, you can assign the result to an IconPropertiesGroup, like so: + * `icon: icon.fromControlsIcon(control.icon)`. + */ + function fromControlsIcon(icon) { + name = Qt.binding(() => icon.name) + source = Qt.binding(() => icon.source) + color = Qt.binding(() => icon.color) + width = Qt.binding(() => icon.width) + height = Qt.binding(() => icon.height) + return this + } +} diff --git a/src/controls/templates/private/MenuIcon.qml b/src/controls/templates/private/MenuIcon.qml new file mode 100644 index 0000000..0dddcfb --- /dev/null +++ b/src/controls/templates/private/MenuIcon.qml @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +Item { + id: canvas + width: height + height: Kirigami.Units.iconSizes.smallMedium + property Kirigami.OverlayDrawer drawer + property color color: Kirigami.Theme.textColor + opacity: 0.8 + layer.enabled: true + + LayoutMirroring.enabled: false + LayoutMirroring.childrenInherit: true + Item { + id: iconRoot + anchors { + fill: parent + margins: Kirigami.Units.smallSpacing + } + readonly property int thickness: 2 + readonly property real drawerPosition: drawer ? drawer.position : 0 + + Rectangle { + anchors { + right: parent.right + top: parent.top + topMargin: -iconRoot.thickness/2 * iconRoot.drawerPosition + } + antialiasing: iconRoot.drawerPosition !== 0 + transformOrigin: Item.Right + width: (1 - iconRoot.drawerPosition) * parent.width + iconRoot.drawerPosition * (Math.sqrt(2*(parent.width*parent.width))) + height: iconRoot.thickness + color: canvas.color + rotation: -45 * iconRoot.drawerPosition + } + + Rectangle { + anchors.centerIn: parent + width: parent.width - parent.width * iconRoot.drawerPosition + height: iconRoot.thickness + color: canvas.color + } + + Rectangle { + anchors { + right: parent.right + bottom: parent.bottom + bottomMargin: -iconRoot.thickness/2 * iconRoot.drawerPosition + } + antialiasing: iconRoot.drawerPosition !== 0 + transformOrigin: Item.Right + width: (1 - iconRoot.drawerPosition) * parent.width + iconRoot.drawerPosition * (Math.sqrt(2*(parent.width*parent.width))) + height: iconRoot.thickness + color: canvas.color + rotation: 45 * iconRoot.drawerPosition + } + } +} diff --git a/src/controls/templates/private/PassiveNotificationsManager.qml b/src/controls/templates/private/PassiveNotificationsManager.qml new file mode 100644 index 0000000..e351869 --- /dev/null +++ b/src/controls/templates/private/PassiveNotificationsManager.qml @@ -0,0 +1,262 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +/** + * @brief PassiveNotificationManager is meant to display small, passive and inline notifications in the app. + * + * It is used to show messages of limited importance that make sense only when + * the user is using the application and wouldn't be suited as a global + * system-wide notification. +*/ +Item { + id: root + + readonly property int maximumNotificationWidth: { + if (Kirigami.Settings.isMobile) { + return applicationWindow().width - Kirigami.Units.largeSpacing * 4 + } else { + return Math.min(Kirigami.Units.gridUnit * 25, applicationWindow().width / 1.5) + } + } + + readonly property int maximumNotificationCount: 4 + + function showNotification(message, timeout, actionText, callBack) { + if (!message) { + return; + } + + let interval = 7000; + + if (timeout === "short") { + interval = 4000; + } else if (timeout === "long") { + interval = 12000; + } else if (timeout > 0) { + interval = timeout; + } + + // this wrapper is necessary because of Qt casting a function into an object + const callBackWrapperObj = callBackWrapper.createObject(listView, { callBack }) + + // set empty string & function for qml not to complain + notificationsModel.append({ + text: message, + actionButtonText: actionText || "", + closeInterval: interval, + callBackWrapper: callBackWrapperObj + }) + // remove the oldest notification if new notification count would exceed 3 + if (notificationsModel.count === maximumNotificationCount) { + if (listView.itemAtIndex(0).hovered === true) { + hideNotification(1) + } else { + hideNotification() + } + } + } + + /** + * @brief Remove a notification at specific index. By default, index is set to 0. + */ + function hideNotification(index = 0) { + if (index >= 0 && notificationsModel.count > index) { + const callBackWrapperObj = notificationsModel.get(index).callBackWrapper + if (callBackWrapperObj) { + callBackWrapperObj.destroy() + } + notificationsModel.remove(index) + } + } + + // we have to set height to show more than one notification + height: Math.min(applicationWindow().height, Kirigami.Units.gridUnit * 10) + + implicitHeight: listView.implicitHeight + implicitWidth: listView.implicitWidth + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Complementary + + ListModel { + id: notificationsModel + } + + ListView { + id: listView + + anchors.fill: parent + anchors.bottomMargin: Kirigami.Units.largeSpacing + + implicitWidth: root.maximumNotificationWidth + spacing: Kirigami.Units.smallSpacing + model: notificationsModel + verticalLayoutDirection: ListView.BottomToTop + keyNavigationEnabled: false + reuseItems: false // do not resue items, otherwise delegates do not hide themselves properly + focus: false + interactive: false + + add: Transition { + id: addAnimation + ParallelAnimation { + alwaysRunToEnd: true + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: Kirigami.Units.longDuration + easing.type: Easing.OutCubic + } + NumberAnimation { + property: "y" + from: addAnimation.ViewTransition.destination.y - Kirigami.Units.gridUnit * 3 + duration: Kirigami.Units.longDuration + easing.type: Easing.OutCubic + } + } + } + displaced: Transition { + ParallelAnimation { + alwaysRunToEnd: true + NumberAnimation { + property: "y" + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + NumberAnimation { + property: "opacity" + duration: 0 + to: 1 + } + } + } + remove: Transition { + ParallelAnimation { + alwaysRunToEnd: true + NumberAnimation { + property: "opacity" + from: 1 + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.InCubic + } + NumberAnimation { + property: "y" + to: Kirigami.Units.gridUnit * 3 + duration: Kirigami.Units.longDuration + easing.type: Easing.InCubic + } + PropertyAction { + property: "transformOrigin" + value: Item.Bottom + } + PropertyAnimation { + property: "scale" + from: 1 + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.InCubic + } + } + } + delegate: QQC2.Control { + id: delegate + + hoverEnabled: true + + anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined + width: Math.min(implicitWidth, maximumNotificationWidth) + implicitHeight: { + // HACK: contentItem.implicitHeight needs to be updated manually for some reason + void contentItem.implicitHeight; + return Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding); + } + z: { + if (delegate.hovered) { + return 2; + } else if (delegate.index === 0) { + return 1; + } else { + return 0; + } + } + + leftPadding: Kirigami.Units.largeSpacing + rightPadding: Kirigami.Units.largeSpacing + topPadding: Kirigami.Units.largeSpacing + bottomPadding: Kirigami.Units.largeSpacing + + contentItem: RowLayout { + id: mainLayout + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: root.Kirigami.Theme.colorSet + + spacing: Kirigami.Units.mediumSpacing + + TapHandler { + acceptedButtons: Qt.LeftButton + onTapped: eventPoint => hideNotification(index) + } + Timer { + id: timer + interval: model.closeInterval + running: !delegate.hovered + onTriggered: hideNotification(index) + } + + QQC2.Label { + id: label + text: model.text + elide: Text.ElideRight + wrapMode: Text.Wrap + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + + QQC2.Button { + id: actionButton + text: model.actionButtonText + visible: text.length > 0 + Layout.alignment: Qt.AlignVCenter + onClicked: { + const callBack = model.callBackWrapper.callBack + hideNotification(index) + if (callBack && (typeof callBack === "function")) { + callBack(); + } + } + } + } + background: Kirigami.ShadowedRectangle { + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: root.Kirigami.Theme.colorSet + shadow { + size: Kirigami.Units.gridUnit/2 + color: Qt.rgba(0, 0, 0, 0.4) + yOffset: 2 + } + radius: Kirigami.Units.cornerRadius + color: Kirigami.Theme.backgroundColor + opacity: 0.9 + } + } + } + Component { + id: callBackWrapper + QtObject { + property var callBack + } + } +} + diff --git a/src/controls/templates/private/qmldir b/src/controls/templates/private/qmldir new file mode 100644 index 0000000..b5e899d --- /dev/null +++ b/src/controls/templates/private/qmldir @@ -0,0 +1,11 @@ +module org.kde.kirigami.templates.private + +BackButton 1.0 BackButton.qml +BorderPropertiesGroup 1.0 BorderPropertiesGroup.qml +ContextIcon 1.0 ContextIcon.qml +DrawerHandle 1.0 DrawerHandle.qml +ForwardButton 1.0 ForwardButton.qml +GenericDrawerIcon 1.0 GenericDrawerIcon.qml +IconPropertiesGroup 1.0 IconPropertiesGroup.qml +MenuIcon 1.0 MenuIcon.qml +PassiveNotificationsManager 1.0 PassiveNotificationsManager.qml diff --git a/src/controls/templates/qmldir b/src/controls/templates/qmldir new file mode 100644 index 0000000..9b9e53e --- /dev/null +++ b/src/controls/templates/qmldir @@ -0,0 +1,9 @@ +module org.kde.kirigami.templates + +AbstractApplicationHeader 2.2 AbstractApplicationHeader.qml +AbstractCard 2.4 AbstractCard.qml +singleton AppHeaderSizeGroup 2.2 SingletonHeaderSizeGroup.qml +Chip 2.19 Chip.qml +InlineMessage 2.4 InlineMessage.qml +OverlayDrawer 2.2 OverlayDrawer.qml +OverlaySheet 2.2 OverlaySheet.qml diff --git a/src/copyhelper.cpp b/src/copyhelper.cpp new file mode 100644 index 0000000..20bd7fc --- /dev/null +++ b/src/copyhelper.cpp @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2009 Alan Alpert + * SPDX-FileCopyrightText: 2010 Ménard Alexis + * SPDX-FileCopyrightText: 2010 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "copyhelper.h" + +#include +#include + +void CopyHelperPrivate::copyTextToClipboard(const QString &text) +{ + qGuiApp->clipboard()->setText(text); +} + +#include "moc_copyhelper.cpp" diff --git a/src/copyhelper.h b/src/copyhelper.h new file mode 100644 index 0000000..e1802bc --- /dev/null +++ b/src/copyhelper.h @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2009 Alan Alpert + * SPDX-FileCopyrightText: 2010 Ménard Alexis + * SPDX-FileCopyrightText: 2010 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef COPYHELPER_H +#define COPYHELPER_H + +#include +#include + +class CopyHelperPrivate : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON +public: + Q_INVOKABLE void copyTextToClipboard(const QString &text); +}; + +#endif diff --git a/src/delegates/CMakeLists.txt b/src/delegates/CMakeLists.txt new file mode 100644 index 0000000..0fcefe4 --- /dev/null +++ b/src/delegates/CMakeLists.txt @@ -0,0 +1,27 @@ + +add_library(KirigamiDelegates) +ecm_add_qml_module(KirigamiDelegates URI "org.kde.kirigami.delegates" + GENERATE_PLUGIN_SOURCE + INSTALLED_PLUGIN_TARGET KF6KirigamiDelegates + DEPENDENCIES QtQuick org.kde.kirigami.platform org.kde.kirigami.primitives +) + +ecm_target_qml_sources(KirigamiDelegates SOURCES + IconTitleSubtitle.qml + TitleSubtitle.qml + + SubtitleDelegate.qml + CheckSubtitleDelegate.qml + RadioSubtitleDelegate.qml + SwitchSubtitleDelegate.qml +) + +set_target_properties(KirigamiDelegates PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION 6 + EXPORT_NAME "KirigamiDelegates" +) + +ecm_finalize_qml_module(KirigamiDelegates EXPORT KirigamiTargets) + +install(TARGETS KirigamiDelegates EXPORT KirigamiTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/delegates/CheckSubtitleDelegate.qml b/src/delegates/CheckSubtitleDelegate.qml new file mode 100644 index 0000000..d2c8e1c --- /dev/null +++ b/src/delegates/CheckSubtitleDelegate.qml @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2023 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami.platform as Platform + +/** + * A convenience wrapper combining QtQuick Controls CheckDelegate and IconTitleSubtitle + * + * This is an intentionally minimal wrapper that replaces the CheckDelegate's + * contentItem with an IconTitleSubtitle and adds a subtitle property. + * + * If you wish to customize the layout further, create your own `CheckDelegate` + * subclass with the `contentItem:` property set to the content of your choice. + * This can include `IconTitleSubtitle` inside a Layout, for example. + * + * \note If you don't need a subtitle, use `CheckDelegate` directly. + * + * \sa Kirigami::Delegates::TitleSubtitle + * \sa Kirigami::Delegates::IconTitleSubtitle + */ +QQC2.CheckDelegate { + id: delegate + + // Please see the developer note in ItemDelegate + + /** + * The subtitle to display. + */ + property string subtitle + + QQC2.ToolTip.text: text + (subtitle.length > 0 ? "\n\n" + subtitle : "") + QQC2.ToolTip.visible: (Platform.Settings.tabletMode ? down : hovered) && (contentItem?.truncated ?? false) + QQC2.ToolTip.delay: Platform.Units.toolTipDelay + + contentItem: IconTitleSubtitle { + icon: icon.fromControlsIcon(delegate.icon) + title: delegate.text + subtitle: delegate.subtitle + selected: delegate.highlighted || delegate.down + font: delegate.font + } +} diff --git a/src/delegates/IconTitleSubtitle.qml b/src/delegates/IconTitleSubtitle.qml new file mode 100644 index 0000000..580799d --- /dev/null +++ b/src/delegates/IconTitleSubtitle.qml @@ -0,0 +1,164 @@ +/* + * SPDX-FileCopyrightText: 2010 Marco Martin + * SPDX-FileCopyrightText: 2022 ivan tkachenko + * SPDX-FileCopyrightText: 2023 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami.platform as Platform +import org.kde.kirigami.primitives as Primitives +import org.kde.kirigami.templates.private as KTP + +/** + * A simple item containing an icon, title and subtitle. + * + * This is an extension of TitleSubtitle that adds an icon to the side. + * It is intended as a contentItem for ItemDelegate and related controls. + * + * When using it as a contentItem, make sure to bind the appropriate properties + * to those of the Control. Prefer binding to the Control's properties over + * setting the properties directly, as the Control's properties may affect other + * things like setting accessible names. + * + * This (and TitleSubtitle) can be combined with other controls in a layout to + * create complex content items for controls. + * + * Example usage creating a CheckDelegate with an extra button on the side: + * + * ```qml + * CheckDelegate { + * id: delegate + * + * text: "Example" + * icon.name: "document-new" + * + * contentItem: RowLayout { + * spacing: Kirigami.Units.smallSpacing + * + * Kirigami.IconTitleSubtitle { + * Layout.fillWidth: true + * + * icon: icon.fromControlsIcon(delegate.icon) + * title: delegate.text + * selected: delegate.highlighted || delegate.down + * font: delegate.font + * } + * + * Button { + * icon.name: "document-open" + * text: "Extra Action" + * } + * } + * } + * ``` + * + * \sa Kirigami::Delegates::TitleSubtitle + * \sa Kirigami::Delegates::ItemDelegate + */ +Item { + id: root + + /** + * @copydoc Kirigami::TitleSubtitle::title + */ + required property string title + /** + * @copydoc Kirigami::TitleSubtitle::subtitle + */ + property alias subtitle: titleSubtitle.subtitle + /** + * @copydoc Kirigami::TitleSubtitle::color + */ + property alias color: titleSubtitle.color + /** + * @copydoc Kirigami::TitleSubtitle::subtitleColor + */ + property alias subtitleColor: titleSubtitle.subtitleColor + /** + * @copydoc Kirigami::TitleSubtitle::font + */ + property alias font: titleSubtitle.font + /** + * @copydoc Kirigami::TitleSubtitle::subtitleFont + */ + property alias subtitleFont: titleSubtitle.subtitleFont + /** + * @copydoc Kirigami::TitleSubtitle::reserveSpaceForSubtitle + */ + property alias reserveSpaceForSubtitle: titleSubtitle.reserveSpaceForSubtitle + /** + * @copydoc Kirigami::TitleSubtitle::selected + */ + property alias selected: titleSubtitle.selected + /** + * @copydoc Kirigami::TitleSubtitle::elide + */ + property alias elide: titleSubtitle.elide + /** + * @copydoc Kirigami::TitleSubtitle::wrapMode + */ + property alias wrapMode: titleSubtitle.wrapMode + /** + * @copydoc Kirigami::TitleSubtitle::truncated + */ + property alias truncated: titleSubtitle.truncated + + /** + * Grouped property for icon properties. + * + * \note By default, IconTitleSubtitle will reserve the space for the icon, + * even if it is not set. To remove that space, set `icon.width` to 0. + */ + property KTP.IconPropertiesGroup icon: KTP.IconPropertiesGroup { + width: titleSubtitle.subtitleVisible ? Platform.Units.iconSizes.medium : Platform.Units.iconSizes.smallMedium + height: width + } + + /** + * @copydoc Kirigami::TitleSubtitle::linkActivated + */ + signal linkActivated(string link) + + /** + * @copydoc Kirigami::TitleSubtitle::linkHovered + */ + signal linkHovered(string link) + + implicitWidth: iconItem.implicitWidth + titleSubtitle.anchors.leftMargin + titleSubtitle.implicitWidth + implicitHeight: Math.max(iconItem.implicitHeight, titleSubtitle.implicitHeight) + + Primitives.Icon { + id: iconItem + + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + + source: root.icon.name.length > 0 ? root.icon.name : root.icon.source + implicitWidth: root.icon.width + implicitHeight: root.icon.height + selected: root.selected + color: root.icon.color + } + + TitleSubtitle { + id: titleSubtitle + + anchors { + left: iconItem.right + leftMargin: root.icon.width > 0 ? Platform.Units.mediumSpacing : 0 + top: parent.top + bottom: parent.bottom + right: parent.right + } + + title: root.title + + onLinkActivated: link => root.linkActivated(link) + onLinkHovered: link => root.linkHovered(link) + } +} diff --git a/src/delegates/README.md b/src/delegates/README.md new file mode 100644 index 0000000..c9a83bb --- /dev/null +++ b/src/delegates/README.md @@ -0,0 +1,12 @@ +# Kirigami Delegates Module + +This module contains custom delegates and types used to build delegates from. + +# What goes here + +The following criteria should be used to determine if a type belongs here: + +- Custom delegate types. +- Types used to build custom delegate types. +- Types are allowed to depend on QtQuick Controls. +- Types are only allowed to depend on the Platform and Primitives submodules. diff --git a/src/delegates/RadioSubtitleDelegate.qml b/src/delegates/RadioSubtitleDelegate.qml new file mode 100644 index 0000000..20722c2 --- /dev/null +++ b/src/delegates/RadioSubtitleDelegate.qml @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2023 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 + +import org.kde.kirigami.platform as Platform + +/** + * A convenience wrapper combining QtQuick Controls RadioDelegate and IconTitleSubtitle + * + * This is an intentionally minimal wrapper that replaces the RadioDelegate's + * contentItem with an IconTitleSubtitle and adds a subtitle property. + * + * If you wish to customize the layout further, create your own `RadioDelegate` + * subclass with the `contentItem:` property set to the content of your choice. + * This can include `IconTitleSubtitle` inside a Layout, for example. + * + * \note If you don't need a subtitle, use `RadioDelegate` directly. + * + * \sa Kirigami::Delegates::TitleSubtitle + * \sa Kirigami::Delegates::IconTitleSubtitle + */ +QQC2.RadioDelegate { + id: delegate + + // Please see the developer note in ItemDelegate + + /** + * The subtitle to display. + */ + property string subtitle + + QQC2.ToolTip.text: text + (subtitle.length > 0 ? "\n\n" + subtitle : "") + QQC2.ToolTip.visible: (Platform.Settings.tabletMode ? down : hovered) && (contentItem?.truncated ?? false) + QQC2.ToolTip.delay: Platform.Units.toolTipDelay + + contentItem: IconTitleSubtitle { + icon: icon.fromControlsIcon(delegate.icon) + title: delegate.text + subtitle: delegate.subtitle + selected: delegate.highlighted || delegate.down + font: delegate.font + } +} diff --git a/src/delegates/SubtitleDelegate.qml b/src/delegates/SubtitleDelegate.qml new file mode 100644 index 0000000..a6a898c --- /dev/null +++ b/src/delegates/SubtitleDelegate.qml @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2023 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami.platform as Platform + +/** + * A convenience wrapper combining QtQuick Controls ItemDelegate and IconTitleSubtitle + * + * This is an intentionally minimal wrapper that replaces the ItemDelegate's + * contentItem with an IconTitleSubtitle and adds a subtitle property. + * + * If you wish to customize the layout further, create your own `ItemDelegate` + * subclass with the `contentItem:` property set to the content of your choice. + * This can include `IconTitleSubtitle` inside a Layout, for example. + * + * \note If you don't need a subtitle, use `ItemDelegate` directly. + * + * \sa Kirigami::Delegates::TitleSubtitle + * \sa Kirigami::Delegates::IconTitleSubtitle + */ +QQC2.ItemDelegate { + id: delegate + + // Developer note: This is intentional kept incredibly minimal as we want to + // reuse as much of upstream ItemDelegate as possible, the only extra thing + // being the subtitle property. Should that ever become an upstream feature, + // these controls will be removed in favour of using upstream's implementation + // directly. + + /** + * The subtitle to display. + */ + property string subtitle + + QQC2.ToolTip.text: text + (subtitle.length > 0 ? "\n\n" + subtitle : "") + QQC2.ToolTip.visible: (Platform.Settings.tabletMode ? down : hovered) && (contentItem?.truncated ?? false) + QQC2.ToolTip.delay: Platform.Units.toolTipDelay + + contentItem: IconTitleSubtitle { + icon: icon.fromControlsIcon(delegate.icon) + title: delegate.text + subtitle: delegate.subtitle + selected: delegate.highlighted || delegate.down + font: delegate.font + } +} diff --git a/src/delegates/SwitchSubtitleDelegate.qml b/src/delegates/SwitchSubtitleDelegate.qml new file mode 100644 index 0000000..c73ab11 --- /dev/null +++ b/src/delegates/SwitchSubtitleDelegate.qml @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2023 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami.platform as Platform + +/** + * A convenience wrapper combining QtQuick Controls SwitchDelegate and IconTitleSubtitle + * + * This is an intentionally minimal wrapper that replaces the SwitchDelegate's + * contentItem with an IconTitleSubtitle and adds a subtitle property. + * + * If you wish to customize the layout further, create your own `SwitchDelegate` + * subclass with the `contentItem:` property set to the content of your choice. + * This can include `IconTitleSubtitle` inside a Layout, for example. + * + * \note If you don't need a subtitle, use `SwitchDelegate` directly. + * + * \sa Kirigami::Delegates::TitleSubtitle + * \sa Kirigami::Delegates::IconTitleSubtitle + */ +QQC2.SwitchDelegate { + id: delegate + + // Please see the developer note in ItemDelegate + + /** + * The subtitle to display. + */ + property string subtitle + + QQC2.ToolTip.text: text + (subtitle.length > 0 ? "\n\n" + subtitle : "") + QQC2.ToolTip.visible: (Platform.Settings.tabletMode ? down : hovered) && (contentItem?.truncated ?? false) + QQC2.ToolTip.delay: Platform.Units.toolTipDelay + + contentItem: IconTitleSubtitle { + icon: icon.fromControlsIcon(delegate.icon) + title: delegate.text + subtitle: delegate.subtitle + selected: delegate.highlighted || delegate.down + font: delegate.font + } +} diff --git a/src/delegates/TitleSubtitle.qml b/src/delegates/TitleSubtitle.qml new file mode 100644 index 0000000..0276c28 --- /dev/null +++ b/src/delegates/TitleSubtitle.qml @@ -0,0 +1,185 @@ +/* + * SPDX-FileCopyrightText: 2010 Marco Martin + * SPDX-FileCopyrightText: 2022 ivan tkachenko + * SPDX-FileCopyrightText: 2023 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami.platform as Platform + +/** + * A simple item containing a title and subtitle label. + * + * This is mainly intended as a replacement for a list delegate content item, + * but can be used as a replacement for other content items as well. + * + * When using it as a contentItem, make sure to bind the appropriate properties + * to those of the Control. Prefer binding to the Control's properties over + * setting the properties directly, as the Control's properties may affect other + * things like setting accessible names. + * + * Example usage as contentItem of an ItemDelegate: + * + * ```qml + * ItemDelegate { + * id: delegate + * + * text: "Example" + * + * contentItem: Kirigami.TitleSubtitle { + * title: delegate.text + * subtitle: "This is an example." + * font: delegate.font + * selected: delegate.highlighted || delegate.down + * } + * } + * ``` + * + * \sa Kirigami::Delegates::IconTitleSubtitle + * \sa Kirigami::Delegates::ItemDelegate + */ +Item { + id: root + + /** + * The title to display. + */ + required property string title + /** + * The subtitle to display. + */ + property string subtitle + /** + * The color to use for the title. + * + * By default this is `Kirigami.Theme.textColor` unless `selected` is true + * in which case this is `Kirigami.Theme.highlightedTextColor`. + */ + property color color: selected ? Platform.Theme.highlightedTextColor : Platform.Theme.textColor + /** + * The color to use for the subtitle. + * + * By default this is `color` mixed with the background color. + */ + property color subtitleColor: selected + ? Platform.Theme.highlightedTextColor + : Platform.ColorUtils.linearInterpolation(color, Platform.Theme.backgroundColor, 0.3) + /** + * The font used to display the title. + */ + property font font: Platform.Theme.defaultFont + /** + * The font used to display the subtitle. + */ + property font subtitleFont: Platform.Theme.smallFont + /** + * The text elision mode used for both the title and subtitle. + */ + property int elide: Text.ElideRight + /** + * The text wrap mode used for both the title and subtitle. + */ + property int wrapMode: Text.NoWrap + /** + * Make the implicit height use the subtitle's height even if no subtitle is set. + */ + property bool reserveSpaceForSubtitle: false + /** + * Should this item be displayed in a selected style? + */ + property bool selected: false + /** + * Is the subtitle visible? + */ + // Note: Don't rely on subtitleItem.visible because visibility is an + // implicitly propagated property, and we don't wanna re-layout on + // hide/show events. Copy-paste its bound expression instead. + readonly property bool subtitleVisible: subtitle.length > 0 || reserveSpaceForSubtitle + /** + * Is the title or subtitle truncated? + */ + readonly property bool truncated: labelItem.truncated || subtitleItem.truncated + + /** + * @brief Emitted when the user clicks on a link embedded in the text of the title or subtitle. + */ + signal linkActivated(string link) + + /** + * @brief Emitted when the user hovers on a link embedded in the text of the title or subtitle. + */ + signal linkHovered(string link) + + implicitWidth: Math.max(labelItem.implicitWidth, subtitleItem.implicitWidth) + implicitHeight: labelItem.implicitHeight + (subtitleVisible ? subtitleItem.implicitHeight : 0) + + Text { + id: labelItem + + anchors { + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter + } + + // Switch off here as this is expected to be set in the base component. + Accessible.ignored: true + + text: root.title + color: root.color + font: root.font + elide: root.elide + wrapMode: root.wrapMode + + onLinkActivated: link => root.linkActivated(link) + onLinkHovered: link => root.linkHovered(link) + + // Work around Qt bug where left aligned text is not right aligned + // in RTL mode unless horizontalAlignment is explicitly set. + // https://bugreports.qt.io/browse/QTBUG-95873 + horizontalAlignment: Text.AlignLeft + + // Note: Can't do this through ordinary bindings as the order between + // binding evaluation is not defined which leads to incorrect sizing or + // the QML engine complaining about not being able to anchor to null items. + states: State { + // Note: Same thing about visibility as in subtitleVisible above. + when: root.subtitle.length > 0 + AnchorChanges { + target: labelItem + anchors.verticalCenter: undefined + anchors.bottom: subtitleItem.top + } + } + } + + Text { + id: subtitleItem + + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + + text: root.subtitle + color: root.subtitleColor + font: root.subtitleFont + elide: root.elide + wrapMode: root.wrapMode + + visible: text.length > 0 + + onLinkActivated: link => root.linkActivated(link) + onLinkHovered: link => root.linkHovered(link) + + // Work around Qt bug where left aligned text is not right aligned + // in RTL mode unless horizontalAlignment is explicitly set. + // https://bugreports.qt.io/browse/QTBUG-95873 + horizontalAlignment: Text.AlignLeft + + renderType: Text.NativeRendering + } +} diff --git a/src/dialogs/CMakeLists.txt b/src/dialogs/CMakeLists.txt new file mode 100644 index 0000000..9bf3cde --- /dev/null +++ b/src/dialogs/CMakeLists.txt @@ -0,0 +1,33 @@ + +add_library(KirigamiDialogs) +ecm_add_qml_module(KirigamiDialogs URI "org.kde.kirigami.dialogs" + VERSION 2.0 + GENERATE_PLUGIN_SOURCE + INSTALLED_PLUGIN_TARGET KF6KirigamiDialogsplugin + DEPENDENCIES QtQuick org.kde.kirigami.platform +) + + + +ecm_target_qml_sources(KirigamiDialogs SOURCES + DialogHeader.qml + DialogHeaderTopContent.qml + Dialog.qml + MenuDialog.qml + PromptDialog.qml + SearchDialog.qml +) + +set_target_properties(KirigamiDialogs PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION 6 + EXPORT_NAME "KirigamiDialogs" +) + +target_include_directories(KirigamiDialogs PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) + +target_link_libraries(KirigamiDialogs PRIVATE Qt6::Quick KirigamiPlatform) + +ecm_finalize_qml_module(KirigamiDialogs EXPORT KirigamiTargets) + +install(TARGETS KirigamiDialogs EXPORT KirigamiTargets ${KF_INSTALL_DEFAULT_ARGUMENTS}) diff --git a/src/dialogs/Dialog.qml b/src/dialogs/Dialog.qml new file mode 100644 index 0000000..6bf507b --- /dev/null +++ b/src/dialogs/Dialog.qml @@ -0,0 +1,493 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + SPDX-FileCopyrightText: 2021 Noah Davis + SPDX-FileCopyrightText: 2022 ivan tkachenko + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQml +import QtQuick.Layouts +import QtQuick.Templates as T +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import org.kde.kirigami.dialogs as KDialogs + +/** + * @brief Popup dialog that is used for short tasks and user interaction. + * + * Dialog consists of three components: the header, the content, + * and the footer. + * + * By default, the header is a heading with text specified by the + * `title` property. + * + * By default, the footer consists of a row of buttons specified by + * the `standardButtons` and `customFooterActions` properties. + * + * The `implicitHeight` and `implicitWidth` of the dialog contentItem is + * the primary hint used for the dialog size. The dialog will be the + * minimum size required for the header, footer and content unless + * it is larger than `maximumHeight` and `maximumWidth`. Use + * `preferredHeight` and `preferredWidth` in order to manually specify + * a size for the dialog. + * + * If the content height exceeds the maximum height of the dialog, the + * dialog's contents will become scrollable. + * + * If the contentItem is a ListView, the dialog will take care of the + * necessary scrollbars and scrolling behaviour. Do not attempt + * to nest ListViews (it must be the top level item), as the scrolling + * behaviour will not be handled. Use ListView's `header` and `footer` instead. + * + * Example for a selection dialog: + * + * @code{.qml} + * import QtQuick + * import QtQuick.Layouts + * import QtQuick.Controls as QQC2 + * import org.kde.kirigami as Kirigami + * + * Kirigami.Dialog { + * title: i18n("Dialog") + * padding: 0 + * preferredWidth: Kirigami.Units.gridUnit * 16 + * + * standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel + * + * onAccepted: console.log("OK button pressed") + * onRejected: console.log("Rejected") + * + * ColumnLayout { + * spacing: 0 + * Repeater { + * model: 5 + * delegate: QQC2.CheckDelegate { + * topPadding: Kirigami.Units.smallSpacing * 2 + * bottomPadding: Kirigami.Units.smallSpacing * 2 + * Layout.fillWidth: true + * text: modelData + * } + * } + * } + * } + * @endcode + * + * Example with scrolling (ListView scrolling behaviour is handled by the Dialog): + * + * @code{.qml} + * import QtQuick + * import QtQuick.Layouts + * import QtQuick.Controls as QQC2 + * import org.kde.kirigami as Kirigami + * + * Kirigami.Dialog { + * id: scrollableDialog + * title: i18n("Select Number") + * + * ListView { + * id: listView + * // hints for the dialog dimensions + * implicitWidth: Kirigami.Units.gridUnit * 16 + * implicitHeight: Kirigami.Units.gridUnit * 16 + * + * model: 100 + * delegate: QQC2.RadioDelegate { + * topPadding: Kirigami.Units.smallSpacing * 2 + * bottomPadding: Kirigami.Units.smallSpacing * 2 + * implicitWidth: listView.width + * text: modelData + * } + * } + * } + * @endcode + * + * There are also sub-components of the Dialog that target specific usecases, + * and can reduce boilerplate code if used: + * + * @see PromptDialog + * @see MenuDialog + * + * @inherit QtQuick.QtObject + */ +T.Dialog { + id: root + + /** + * @brief This property holds the dialog's contents; includes Items and QtObjects. + * @property list dialogData + */ + default property alias dialogData: contentControl.contentData + + /** + * @brief This property holds the content items of the dialog. + * + * The initial height and width of the dialog is calculated from the + * `implicitWidth` and `implicitHeight` of the content. + * + * @property list dialogChildren + */ + property alias dialogChildren: contentControl.contentChildren + + /** + * @brief This property sets the absolute maximum height the dialog can have. + * + * The height restriction is solely applied on the content, so if the + * maximum height given is not larger than the height of the header and + * footer, it will be ignored. + * + * This is the window height, subtracted by largeSpacing on both the top + * and bottom. + */ + readonly property real absoluteMaximumHeight: ((parent && parent.height) || Infinity) - Kirigami.Units.largeSpacing * 2 + + /** + * @brief This property holds the absolute maximum width the dialog can have. + * + * By default, it is the window width, subtracted by largeSpacing on both + * the top and bottom. + */ + readonly property real absoluteMaximumWidth: ((parent && parent.width) || Infinity) - Kirigami.Units.largeSpacing * 2 + + readonly property real __borderWidth: 1 + + /** + * @brief This property holds the maximum height the dialog can have + * (including the header and footer). + * + * The height restriction is solely enforced on the content, so if the + * maximum height given is not larger than the height of the header and + * footer, it will be ignored. + * + * By default, this is `absoluteMaximumHeight`. + */ + property real maximumHeight: absoluteMaximumHeight + + /** + * @brief This property holds the maximum width the dialog can have. + * + * By default, this is `absoluteMaximumWidth`. + */ + property real maximumWidth: absoluteMaximumWidth + + /** + * @brief This property holds the preferred height of the dialog. + * + * The content will receive a hint for how tall it should be to have + * the dialog to be this height. + * + * If the content, header or footer require more space, then the height + * of the dialog will expand to the necessary amount of space. + */ + property real preferredHeight: -1 + + /** + * @brief This property holds the preferred width of the dialog. + * + * The content will receive a hint for how wide it should be to have + * the dialog be this wide. + * + * If the content, header or footer require more space, then the width + * of the dialog will expand to the necessary amount of space. + */ + property real preferredWidth: -1 + + + /** + * @brief This property holds the component to the left of the footer buttons. + */ + property Component footerLeadingComponent + + /** + * @brief his property holds the component to the right of the footer buttons. + */ + property Component footerTrailingComponent + + /** + * @brief This property sets whether to show the close button in the header. + */ + property bool showCloseButton: true + + /** + * @brief This property sets whether the footer button style should be flat. + */ + property bool flatFooterButtons: false + + /** + * @brief This property holds the custom actions displayed in the footer. + * + * Example usage: + * @code{.qml} + * import QtQuick + * import org.kde.kirigami as Kirigami + * + * Kirigami.PromptDialog { + * id: dialog + * title: i18n("Confirm Playback") + * subtitle: i18n("Are you sure you want to play this song? It's really loud!") + * + * standardButtons: Kirigami.Dialog.Cancel + * customFooterActions: [ + * Kirigami.Action { + * text: i18n("Play") + * icon.name: "media-playback-start" + * onTriggered: { + * //... + * dialog.close(); + * } + * } + * ] + * } + * @endcode + * + * @see org::kde::kirigami::Action + */ + property list customFooterActions + + // DialogButtonBox should NOT contain invisible buttons, because in Qt 6 + // ListView preserves space even for invisible items. + readonly property list __visibleCustomFooterActions: customFooterActions + .filter(action => !(action instanceof Kirigami.Action) || action?.visible) + + function standardButton(button): T.AbstractButton { + // in case a footer is redefined + if (footer instanceof T.DialogButtonBox) { + return footer.standardButton(button); + } else if (footer === footerToolBar) { + return dialogButtonBox.standardButton(button); + } else { + return null; + } + } + + function customFooterButton(action: T.Action): T.AbstractButton { + if (!action) { + // Even if there's a null object in the list of actions, we should + // not return a button for it. + return null; + } + const index = __visibleCustomFooterActions.indexOf(action); + if (index < 0) { + return null; + } + return customFooterButtons.itemAt(index) as T.AbstractButton; + } + + z: Kirigami.OverlayZStacking.z + + // calculate dimensions and in case footer is wider than content, use that + implicitWidth: Math.max(implicitContentWidth, implicitFooterWidth, implicitHeaderWidth) + leftPadding + rightPadding // maximum width enforced from our content (one source of truth) to avoid binding loops + implicitHeight: implicitContentHeight + topPadding + bottomPadding + + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0) + + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0); + + // misc. dialog settings + closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnReleaseOutside + modal: true + clip: false + padding: 0 + horizontalPadding: __borderWidth + padding + + // determine parent so that popup knows which window to popup in + // we want to open the dialog in the center of the window, if possible + Component.onCompleted: { + if (typeof applicationWindow !== "undefined") { + parent = applicationWindow().overlay; + } + } + + // center dialog + x: parent ? Math.round(((parent && parent.width) - width) / 2) : 0 + y: parent ? Math.round(((parent && parent.height) - height) / 2) + Kirigami.Units.gridUnit * 2 * (1 - opacity) : 0 // move animation + + // dialog enter and exit transitions + enter: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1; easing.type: Easing.InOutQuad; duration: Kirigami.Units.longDuration } + } + exit: Transition { + NumberAnimation { property: "opacity"; from: 1; to: 0; easing.type: Easing.InOutQuad; duration: Kirigami.Units.longDuration } + } + + // black background, fades in and out + QQC2.Overlay.modal: Rectangle { + color: Qt.rgba(0, 0, 0, 0.3) + + // the opacity of the item is changed internally by QQuickPopup on open/close + Behavior on opacity { + OpacityAnimator { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + + // dialog view background + background: Kirigami.ShadowedRectangle { + id: rect + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + color: Kirigami.Theme.backgroundColor + radius: Kirigami.Units.cornerRadius + shadow { + size: radius * 2 + color: Qt.rgba(0, 0, 0, 0.3) + yOffset: 1 + } + + border { + width: root.__borderWidth + color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast); + } + } + + // dialog content + contentItem: QQC2.ScrollView { + id: contentControl + + // ensure view colour scheme, and background color + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.View + + QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff + + // height of everything else in the dialog other than the content + property real otherHeights: (root.header?.height ?? 0) + (root.footer?.height ?? 0) + root.topPadding + root.bottomPadding; + + property real calculatedMaximumWidth: Math.min(root.absoluteMaximumWidth, root.maximumWidth) - root.leftPadding - root.rightPadding + property real calculatedMaximumHeight: Math.min(root.absoluteMaximumHeight, root.maximumHeight) - root.topPadding - root.bottomPadding + property real calculatedImplicitWidth: implicitContentWidth + leftPadding + rightPadding + property real calculatedImplicitHeight: implicitContentHeight + topPadding + bottomPadding + + onContentItemChanged: { + const contentFlickable = contentItem as Flickable; + if (contentFlickable) { + /* + Why this is necessary? A Flickable mainItem syncs its size with the contents only on startup, + and if the contents can change their size dinamically afterwards (wrapping text does that), + the contentsize will be wrong see BUG 477257. + + We also don't do this declaratively but only we are sure a contentItem is declared/created as just + accessing the property would create an internal Flickable, making it impossible to assign custom + flickables/listviews to the Dialog. + */ + contentFlickable.contentHeight = Qt.binding(() => calculatedImplicitHeight); + + contentFlickable.clip = true; + } + } + + // how do we deal with the scrollbar width? + // - case 1: the dialog itself has the preferredWidth set + // -> we hint a width to the content so it shrinks to give space to the scrollbar + // - case 2: preferredWidth not set, so we are using the content's implicit width + // -> we expand the dialog's width to accommodate the scrollbar width (to respect the content's desired width) + + // don't enforce preferred width and height if not set (-1), and expand to a larger implicit size + property real preferredWidth: Math.max(root.preferredWidth, calculatedImplicitWidth) + property real preferredHeight: Math.max(root.preferredHeight - otherHeights, calculatedImplicitHeight) + + property real maximumWidth: calculatedMaximumWidth + property real maximumHeight: calculatedMaximumHeight - otherHeights // we enforce maximum height solely from the content + + implicitWidth: Math.min(preferredWidth, maximumWidth) + implicitHeight: Math.min(preferredHeight, maximumHeight) + + // give an implied width and height to the contentItem so that features like word wrapping/eliding work + // cannot placed directly in contentControl as a child, so we must use a property + property var widthHint: Binding { + target: contentControl.contentChildren[0] || null + property: "width" + value: contentControl.width + contentControl.leftPadding + contentControl.rightPadding + restoreMode: Binding.RestoreBinding + } + } + + header: KDialogs.DialogHeader { + dialog: root + contentItem: KDialogs.DialogHeaderTopContent { + dialog: root + } + } + + // use top level control rather than toolbar, since toolbar causes button rendering glitches + footer: T.Control { + id: footerToolBar + + // if there is nothing in the footer, still maintain a height so that we can create a rounded bottom buffer for the dialog + property bool bufferMode: !root.footerLeadingComponent && !dialogButtonBox.visible + implicitHeight: bufferMode ? Math.round(Kirigami.Units.smallSpacing / 2) : implicitContentHeight + topPadding + bottomPadding + implicitWidth: footerLayout.implicitWidth + leftPadding + rightPadding + + padding: !bufferMode ? Kirigami.Units.largeSpacing : 0 + + contentItem: RowLayout { + id: footerLayout + spacing: footerToolBar.spacing + // Don't let user interact with footer during transitions + enabled: root.opened + + Loader { + id: leadingLoader + sourceComponent: root.footerLeadingComponent + } + + // footer buttons + QQC2.DialogButtonBox { + // we don't explicitly set padding, to let the style choose the padding + id: dialogButtonBox + standardButtons: root.standardButtons + visible: count > 0 + padding: 0 + + Layout.fillWidth: true + Layout.alignment: dialogButtonBox.alignment + + position: QQC2.DialogButtonBox.Footer + + // ensure themes don't add a background, since it can lead to visual inconsistencies + // with the rest of the dialog + background: null + + // we need to hook all of the buttonbox events to the dialog events + onAccepted: root.accept() + onRejected: root.reject() + onApplied: root.applied() + onDiscarded: root.discarded() + onHelpRequested: root.helpRequested() + onReset: root.reset() + + // add custom footer buttons + Repeater { + id: customFooterButtons + model: root.__visibleCustomFooterActions + // we have to use Button instead of ToolButton, because ToolButton has no visual distinction when disabled + delegate: QQC2.Button { + required property T.Action modelData + + flat: root.flatFooterButtons + action: modelData + } + } + } + + Loader { + id: trailingLoader + sourceComponent: root.footerTrailingComponent + } + } + + background: Item { + Kirigami.Separator { + id: footerSeparator + visible: if (root.contentItem instanceof T.Pane || root.contentItem instanceof Flickable) { + return root.contentItem.contentHeight > root.implicitContentHeight; + } else { + return false; + } + width: parent.width + anchors.top: parent.top + } + } + } +} diff --git a/src/dialogs/DialogHeader.qml b/src/dialogs/DialogHeader.qml new file mode 100644 index 0000000..2b878b7 --- /dev/null +++ b/src/dialogs/DialogHeader.qml @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + SPDX-FileCopyrightText: 2021 Noah Davis + SPDX-FileCopyrightText: 2022 ivan tkachenko + SPDX-FileCopyrightText: 2025 Nate Graham + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami +import org.kde.kirigami.dialogs as KDialogs + +/** + * @brief Base for a header, to be used as the header: item of a Dialog. + * + * Provides appropriate padding and a bottom separator when the dialog's content + * is scrollable. + * + * Chiefly useful as the base element of a custom header. Example usage for this: + * + * @code{.qml} + * import QtQuick + * import org.kde.kirigami as Kirigami + * import org.kde.kirigami.dialogs as KD + * + * Kirigami.Dialog { + * id: myDialog + * + * title: i18n("My Dialog") + * + * standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel + * + * header: KDialogs.DialogHeader { + * dialog: myDialog + * contentItem: [...] + * } + * [...] + * } + * @endcode + * @inherit T.Control + */ +T.Control { + id: root + + /** + * @brief This property points to the parent dialog, some of whose properties + * need to be available here. + * @property T.Dialog dialog + */ + required property T.Dialog dialog + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + padding: Kirigami.Units.largeSpacing + bottomPadding: verticalPadding + headerSeparator.implicitHeight // add space for bottom separator + + // Bottom separator shown when content is scrollable + background: Item { + Kirigami.Separator { + id: headerSeparator + width: parent.width + anchors.bottom: parent.bottom + visible: if (root.dialog.contentItem instanceof T.Pane || root.dialog.contentItem instanceof Flickable) { + return root.dialog.contentItem.contentHeight > root.dialog.implicitContentHeight; + } else { + return false; + } + } + } + + contentItem: KDialogs.DialogHeaderTopContent { + dialog: root.dialog + } +} diff --git a/src/dialogs/DialogHeaderTopContent.qml b/src/dialogs/DialogHeaderTopContent.qml new file mode 100644 index 0000000..ef5c3e0 --- /dev/null +++ b/src/dialogs/DialogHeaderTopContent.qml @@ -0,0 +1,95 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + SPDX-FileCopyrightText: 2021 Noah Davis + SPDX-FileCopyrightText: 2022 ivan tkachenko + SPDX-FileCopyrightText: 2025 Nate Graham + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Templates as T +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +/** + * @brief Standard top content for a Dialog, including header text and an + * optional close button. + * + * Provides appropriate padding and a bottom separator when the dialog's content + * is scrollable. + * + * Chiefly useful as the first item in a `ColumnLayout` inside a custom header, + * for when you want a custom header that only consists of extra content, and + * does not need to override the standard content. Example usage for a this: + * + * @code{.qml} + * import QtQuick + * import QtQuick.Layouts + * import org.kde.kirigami as Kirigami + * import org.kde.kirigami.dialogs as KDialogs + * + * Kirigami.Dialog { + * id: myDialog + * + * title: i18n("My Dialog") + * + * standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel + * + * header: KDialogs.DialogHeader { + * dialog: myDialog + * + * contentItem: ColumnLayout { + * Spacing: Kirigami.Units.smallSpacing + * + * KDialogs.DialogHeaderTopContent { + * dialog: myDialog + * } + * + * [...] + * } + * } + * [...] + * } + * @endcode + * @inherit T.Control + */ +RowLayout { + id: root + + /** + * @brief This property points to the parent dialog, some of whose properties + * need to be available here. + * @property T.Dialog dialog + */ + required property T.Dialog dialog + + spacing: Kirigami.Units.smallSpacing + + Kirigami.Heading { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + + text: root.dialog.title.length === 0 ? " " : root.dialog.title // always have text to ensure header height + elide: Text.ElideRight + + // use tooltip for long text that is elided + QQC2.ToolTip.visible: truncated && titleHoverHandler.hovered + QQC2.ToolTip.text: root.dialog.title + + HoverHandler { + id: titleHoverHandler + } + } + + QQC2.ToolButton { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + + icon.name: hovered ? "window-close" : "window-close-symbolic" + text: qsTr("Close", "@action:button close dialog") + display: QQC2.AbstractButton.IconOnly + visible: root.dialog.showCloseButton + + onClicked: root.dialog.reject() + } +} diff --git a/src/dialogs/MenuDialog.qml b/src/dialogs/MenuDialog.qml new file mode 100644 index 0000000..6faaf39 --- /dev/null +++ b/src/dialogs/MenuDialog.qml @@ -0,0 +1,130 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + SPDX-FileCopyrightText: 2023 ivan tkachenko + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami + +/** + * A dialog that prompts users with a context menu, with + * list items that perform actions. + * + * Example usage: + * @code{.qml} + * Kirigami.MenuDialog { + * title: i18n("Track Options") + * + * actions: [ + * Kirigami.Action { + * icon.name: "media-playback-start" + * text: i18nc("Start playback of the selected track", "Play") + * tooltip: i18n("Start playback of the selected track") + * }, + * Kirigami.Action { + * enabled: false + * icon.name: "document-open-folder" + * text: i18nc("Show the file for this song in the file manager", "Show in folder") + * tooltip: i18n("Show the file for this song in the file manager") + * }, + * Kirigami.Action { + * icon.name: "documentinfo" + * text: i18nc("Show track metadata", "View details") + * tooltip: i18n("Show track metadata") + * }, + * Kirigami.Action { + * icon.name: "list-add" + * text: i18nc("Add the track to the queue, right after the current track", "Play next") + * tooltip: i18n("Add the track to the queue, right after the current track") + * }, + * Kirigami.Action { + * icon.name: "list-add" + * text: i18nc("Enqueue current track", "Add to queue") + * tooltip: i18n("Enqueue current track") + * } + * ] + * } + * @endcode + * + * @see Dialog + * @see PromptDialog + * @inherit org::kde::kirigami::Dialog + */ +Kirigami.Dialog { + id: root + + /** + * @brief This property holds the actions displayed in the context menu. + */ + property list actions + + /** + * @brief This property holds the content header, which appears above the actions. + * but below the header bar. + */ + property alias contentHeader: columnHeader.contentItem + + /** + * @brief This property holds the content header. + * + * This makes it possible to access its internal properties to, for example, change its padding: + * ``contentHeaderControl.topPadding`` + * + * @property QtQuick.Controls.Control contentHeaderControl + */ + property alias contentHeaderControl: columnHeader + + preferredWidth: Kirigami.Units.gridUnit * 20 + padding: 0 + + ColumnLayout { + id: column + + spacing: 0 + + QQC2.Control { + id: columnHeader + + topPadding: 0 + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + } + + Repeater { + model: root.actions + + delegate: QQC2.ItemDelegate { + required property T.Action modelData + + Layout.fillWidth: true + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + + action: modelData + visible: !(modelData instanceof Kirigami.Action) || modelData.visible + + icon.width: Kirigami.Units.gridUnit + icon.height: Kirigami.Units.gridUnit + + horizontalPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing + leftPadding: undefined + rightPadding: undefined + + QQC2.ToolTip.text: modelData instanceof Kirigami.Action ? modelData.tooltip : "" + QQC2.ToolTip.visible: QQC2.ToolTip.text.length > 0 && (Kirigami.Settings.tabletMode ? pressed : hovered) + QQC2.ToolTip.delay: Kirigami.Settings.tabletMode ? Qt.styleHints.mousePressAndHoldInterval : Kirigami.Units.toolTipDelay + + onClicked: root.close() + } + } + } + + standardButtons: QQC2.DialogButtonBox.NoButton + showCloseButton: true +} diff --git a/src/dialogs/PromptDialog.qml b/src/dialogs/PromptDialog.qml new file mode 100644 index 0000000..a753be4 --- /dev/null +++ b/src/dialogs/PromptDialog.qml @@ -0,0 +1,199 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +/** + * A simple dialog to quickly prompt a user with information, + * and possibly perform an action. + * + * Provides content padding (instead of padding outside of the scroll + * area). Also has a default preferredWidth, as well as the `subtitle` property. + * + * Note: If a `mainItem` is specified, it will replace + * the subtitle label, and so the respective property will have no effect. + * + * @see Dialog + * @see MenuDialog + * + * Example usage: + * + * @code{.qml} + * Kirigami.PromptDialog { + * title: "Reset settings?" + * subtitle: "The stored settings for the application will be deleted, with the defaults restored." + * standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel + * + * onAccepted: console.log("Accepted") + * onRejected: console.log("Rejected") + * } + * @endcode + * + * Text field prompt dialog: + * + * @code{.qml} + * Kirigami.PromptDialog { + * id: textPromptDialog + * title: qsTr("New Folder") + * + * standardButtons: Kirigami.Dialog.NoButton + * customFooterActions: [ + * Kirigami.Action { + * text: qsTr("Create Folder") + * icon.name: "dialog-ok" + * onTriggered: { + * showPassiveNotification("Created"); + * textPromptDialog.close(); + * } + * }, + * Kirigami.Action { + * text: qsTr("Cancel") + * icon.name: "dialog-cancel" + * onTriggered: { + * textPromptDialog.close(); + * } + * } + * ] + * + * QQC2.TextField { + * placeholderText: qsTr("Folder name…") + * } + * } + * @endcode + * + * @inherit Dialog + */ +Kirigami.Dialog { + id: root + + default property alias mainItem: mainLayout.data + + enum DialogType { + Success, + Warning, + Error, + Information, + None + } + + /** + * This property holds the dialogType. It can be either: + * + * - `PromptDialog.Success`: For a sucess message + * - `PromptDialog.Warning`: For a warning message + * - `PromptDialog.Error`: For an actual error + * - `PromptDialog.Information`: For an informational message + * - `PromptDialog.None`: No specific dialog type. + * + * By default, the dialogType is `Kirigami.PromptDialog.None` + */ + property int dialogType: Kirigami.PromptDialog.None + + /** + * The text to use in the dialog's contents. + */ + property string subtitle + + /** + * The padding around the content, within the scroll area. + * + * Default is `Kirigami.Units.largeSpacing`. + */ + property real contentPadding: Kirigami.Units.largeSpacing + + /** + * The top padding of the content, within the scroll area. + */ + property real contentTopPadding: contentPadding + + /** + * The bottom padding of the content, within the scroll area. + */ + property real contentBottomPadding: footer.padding === 0 ? contentPadding : 0 // add bottom padding if there is no footer + + /** + * The left padding of the content, within the scroll area. + */ + property real contentLeftPadding: contentPadding + + /** + * The right padding of the content, within the scroll area. + */ + property real contentRightPadding: contentPadding + + /** + * This property holds the icon name used by the PromptDialog. + */ + property string iconName: switch (dialogType) { + case Kirigami.PromptDialog.Success: + return "data-success"; + case Kirigami.PromptDialog.Warning: + return "data-warning"; + case Kirigami.PromptDialog.Error: + return "data-error"; + case Kirigami.PromptDialog.Information: + return "data-information"; + default: + return ""; + } + + padding: 0 + + header: null + + Kirigami.Padding { + id: wrapper + + topPadding: root.contentTopPadding + leftPadding: root.contentLeftPadding + rightPadding: root.contentRightPadding + bottomPadding: root.contentBottomPadding + + contentItem: RowLayout { + spacing: Kirigami.Units.largeSpacing + + Kirigami.Icon { + source: root.iconName + visible: root.iconName.length > 0 + + Layout.preferredWidth: Kirigami.Units.iconSizes.huge + Layout.preferredHeight: Kirigami.Units.iconSizes.huge + Layout.alignment: Qt.AlignTop + } + + ColumnLayout { + id: mainLayout + + spacing: Kirigami.Units.smallSpacing + + Layout.fillWidth: true + + ColumnLayout { + spacing: 0 + + Kirigami.Heading { + text: root.title + visible: root.title.length > 0 + elide: QQC2.Label.ElideRight + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Kirigami.SelectableLabel { + text: root.subtitle + wrapMode: TextEdit.Wrap + visible: text.length > 0 + Layout.fillWidth: true + } + } + } + } + } +} diff --git a/src/dialogs/SearchDialog.qml b/src/dialogs/SearchDialog.qml new file mode 100644 index 0000000..538e46e --- /dev/null +++ b/src/dialogs/SearchDialog.qml @@ -0,0 +1,224 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-FileCopyrightText: 2024 Carl Schwan +// SPDX-License-Identifier: LGPL-2.0-or-later + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami + +/** + * A dialog to let's you do a global search accross your applications + * documents, chat rooms and more. + * + * Example usage for a chat app where we want to quickly search for a room. + * + * @code{.qml} + * import QtQuick + * import org.kde.kitemmodels as KItemModels + * import org.kde.kirigami as Kirigami + * + * Kirigami.SearchDialog { + * id: root + * + * onTextChanged: { + * sortModel.filterText = text; + * } + * onAccepted: listView.currentItem.clicked() + * + * emptyText: i18nc("Placeholder message", "No room found.") + * + * model: KItemModels.KSortFilterProxyModel { + * id: sortModel + * + * sourceModel: RoomModel { } + * } + * + * delegate: RoomDelegate { + * onClicked: root.close() + * } + * + * Shortcut { + * sequence: "Ctrl+K" + * onActivated: root.open() + * } + * } + * @endcode{} + * + * @image html searchdialog.html + * + * @note This component is unsuitable on mobile. Instead on mobile prefer to + * use a seperate page for the search. + * + * @since Kirigami 6.3 + */ +QQC2.Dialog { + id: root + + /** + * This property holds an alias to the model of the internal ListView. + */ + property alias model: listView.model + + /** + * This property holds an alias to the delegate component of the internal ListView. + */ + property alias delegate: listView.delegate + + /** + * This property holds an alias to the currentItem component of the internal ListView. + */ + property alias currentItem: listView.currentItem + + /** + * This property holds an alias to the section component of the internal ListView. + */ + property alias section: listView.section + + /** + * This property holds an alias to the content of the search field. + */ + property alias text: searchField.text + + /** + * This property holds an alias to the left actions of the seach field. + */ + property alias searchFieldLeftActions: searchField.leftActions + + /** + * This property holds an alias to the right actions of the seach field. + */ + property alias searchFieldRightActions: searchField.rightActions + + /** + * The placeholder text shown in the (empty) search field. + */ + property alias searchFieldPlaceholderText: searchField.placeholderText + + /** + * This property holds the number of search results displayed in the internal ListView. + */ + property alias count: listView.count + + /** + * This property holds an alias to the placeholder message text displayed + * when the internal list view is empty. + */ + property alias emptyText: placeholder.text + + /** + * This property holds an alias to the placeholder message icon displayed + * when the internal list view is empty. + */ + property alias emptyIcon: placeholder.icon + + /** + * @brief Helpful action when the list is empty + * + * This property holds an alias to the helpful action of the placeholder message + * when the internal list view is empty. + * + * @since 6.10 + */ + property alias emptyHelpfulAction: placeholder.helpfulAction + + width: Math.min(Kirigami.Units.gridUnit * 35, parent.width) + height: Math.min(Kirigami.Units.gridUnit * 20, parent.height) + + padding: 0 + + anchors.centerIn: parent + + modal: true + + onOpened: { + searchField.forceActiveFocus(); + searchField.text = ""; + listView.currentIndex = 0; + } + + contentItem: ColumnLayout { + spacing: 0 + + Kirigami.SearchField { + id: searchField + + Layout.fillWidth: true + + background: null + + Layout.margins: Kirigami.Units.smallSpacing + + Keys.onDownPressed: { + const listViewHadFocus = listView.activeFocus; + listView.forceActiveFocus(); + if (listView.currentIndex < listView.count - 1) { + // don't move to the next entry when we just changed focus from the search field to the list view + if (listViewHadFocus) { + listView.currentIndex++; + } + } else { + listView.currentIndex = 0; + } + } + Keys.onUpPressed: { + listView.forceActiveFocus(); + if (listView.currentIndex === 0) { + listView.currentIndex = listView.count - 1; + } else { + listView.currentIndex--; + } + } + Keys.onPressed: (event) => { + switch (event.key) { + case Qt.Key_PageDown: + listView.forceActiveFocus(); + listView.currentIndex = Math.min(listView.count - 1, listView.currentIndex + Math.floor(listView.height / listView.currentItem.height)); + event.accepted = true; + break; + case Qt.Key_PageUp: + listView.forceActiveFocus(); + listView.currentIndex = Math.max(0, listView.currentIndex - Math.floor(listView.height / listView.currentItem.height)); + event.accepted = true; + break; + } + } + + focusSequence: "" + autoAccept: false + + onAccepted: root.accepted() + } + + Kirigami.Separator { + Layout.fillWidth: true + } + + QQC2.ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + Keys.forwardTo: searchField + + ListView { + id: listView + + currentIndex: 0 + clip: true + highlightMoveDuration: 200 + Keys.forwardTo: searchField + keyNavigationEnabled: true + + Kirigami.PlaceholderMessage { + id: placeholder + anchors.centerIn: parent + width: parent.width - Kirigami.Units.gridUnit * 4 + icon.name: 'system-search-symbolic' + visible: listView.count === 0 && text.length > 0 + } + } + } + } +} diff --git a/src/enums.h b/src/enums.h new file mode 100644 index 0000000..1f189d9 --- /dev/null +++ b/src/enums.h @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef ENUMS_H +#define ENUMS_H + +#include +#include + +namespace ApplicationHeaderStyle +{ +Q_NAMESPACE +QML_ELEMENT + +enum Status { + Auto = 0, + Breadcrumb, + Titles, + ToolBar, ///@since 5.48 + None, ///@since 5.48 +}; +Q_ENUM_NS(Status) + +enum NavigationButton { + NoNavigationButtons = 0, + ShowBackButton = 0x1, + ShowForwardButton = 0x2, +}; +Q_ENUM_NS(NavigationButton) +Q_DECLARE_FLAGS(NavigationButtons, NavigationButton) +} + +namespace MessageType +{ +Q_NAMESPACE +QML_ELEMENT + +enum Type { + Information = 0, + Positive, + Warning, + Error, +}; +Q_ENUM_NS(Type) +}; + +#endif // ENUMS_H diff --git a/src/imagecolors.cpp b/src/imagecolors.cpp new file mode 100644 index 0000000..4b6b463 --- /dev/null +++ b/src/imagecolors.cpp @@ -0,0 +1,684 @@ +/* + * SPDX-FileCopyrightText: 2020 Marco Martin + * SPDX-FileCopyrightText: 2024 ivan tkachenko + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "imagecolors.h" + +#include +#include +#include +#include + +#include "loggingcategory.h" +#include +#include + +#include "config-OpenMP.h" +#if HAVE_OpenMP +#include +#endif + +#include "platform/platformtheme.h" + +#define return_fallback(value) \ + if (m_imageData.m_samples.size() == 0) { \ + return value; \ + } + +#define return_fallback_finally(value, finally) \ + if (m_imageData.m_samples.size() == 0) { \ + return value.isValid() \ + ? value \ + : static_cast(qmlAttachedPropertiesObject(this, true))->finally(); \ + } + +PaletteSwatch::PaletteSwatch() +{ +} + +PaletteSwatch::PaletteSwatch(qreal ratio, const QColor &color, const QColor &contrastColor) + : m_ratio(ratio) + , m_color(color) + , m_contrastColor(contrastColor) +{ +} + +qreal PaletteSwatch::ratio() const +{ + return m_ratio; +} + +const QColor &PaletteSwatch::color() const +{ + return m_color; +} + +const QColor &PaletteSwatch::contrastColor() const +{ + return m_contrastColor; +} + +bool PaletteSwatch::operator==(const PaletteSwatch &other) const +{ + return m_ratio == other.m_ratio // + && m_color == other.m_color // + && m_contrastColor == other.m_contrastColor; +} + +ImageColors::ImageColors(QObject *parent) + : QObject(parent) +{ +} + +ImageColors::~ImageColors() +{ +} + +void ImageColors::setSource(const QVariant &source) +{ + if (m_futureSourceImageData) { + m_futureSourceImageData->cancel(); + m_futureSourceImageData->deleteLater(); + m_futureSourceImageData = nullptr; + } + + if (source.canConvert()) { + setSourceItem(source.value()); + } else if (source.canConvert()) { + setSourceImage(source.value()); + } else if (source.canConvert()) { + setSourceImage(source.value().pixmap(128, 128).toImage()); + } else if (source.canConvert()) { + const QString sourceString = source.toString(); + + if (QIcon::hasThemeIcon(sourceString)) { + setSourceImage(QIcon::fromTheme(sourceString).pixmap(128, 128).toImage()); + } else { + QFuture future = QtConcurrent::run([sourceString]() { + if (auto url = QUrl(sourceString); url.isLocalFile()) { + return QImage(url.toLocalFile()); + } + return QImage(sourceString); + }); + m_futureSourceImageData = new QFutureWatcher(this); + connect(m_futureSourceImageData, &QFutureWatcher::finished, this, [this, source]() { + const QImage image = m_futureSourceImageData->future().result(); + m_futureSourceImageData->deleteLater(); + m_futureSourceImageData = nullptr; + setSourceImage(image); + m_source = source; + Q_EMIT sourceChanged(); + }); + m_futureSourceImageData->setFuture(future); + return; + } + } else { + return; + } + + m_source = source; + Q_EMIT sourceChanged(); +} + +QVariant ImageColors::source() const +{ + return m_source; +} + +void ImageColors::setSourceImage(const QImage &image) +{ + if (m_window) { + disconnect(m_window.data(), nullptr, this, nullptr); + } + if (m_sourceItem) { + disconnect(m_sourceItem.data(), nullptr, this, nullptr); + } + if (m_grabResult) { + disconnect(m_grabResult.data(), nullptr, this, nullptr); + m_grabResult.clear(); + } + + m_sourceItem.clear(); + + m_sourceImage = image; + update(); +} + +QImage ImageColors::sourceImage() const +{ + return m_sourceImage; +} + +void ImageColors::setSourceItem(QQuickItem *source) +{ + if (m_sourceItem == source) { + return; + } + + if (m_window) { + disconnect(m_window.data(), nullptr, this, nullptr); + } + if (m_sourceItem) { + disconnect(m_sourceItem, nullptr, this, nullptr); + } + m_sourceItem = source; + update(); + + if (m_sourceItem) { + auto syncWindow = [this]() { + if (m_window) { + disconnect(m_window.data(), nullptr, this, nullptr); + } + m_window = m_sourceItem->window(); + if (m_window) { + connect(m_window, &QWindow::visibleChanged, this, &ImageColors::update); + } + update(); + }; + + connect(m_sourceItem, &QQuickItem::windowChanged, this, syncWindow); + syncWindow(); + } +} + +QQuickItem *ImageColors::sourceItem() const +{ + return m_sourceItem; +} + +void ImageColors::update() +{ + if (m_futureImageData) { + m_futureImageData->disconnect(this, nullptr); + m_futureImageData->cancel(); + m_futureImageData->deleteLater(); + m_futureImageData = nullptr; + } + + auto runUpdate = [this]() { + auto sourceImage{m_sourceImage}; + QFuture future = QtConcurrent::run([sourceImage = std::move(sourceImage)]() { + return generatePalette(sourceImage); + }); + m_futureImageData = new QFutureWatcher(this); + connect(m_futureImageData, &QFutureWatcher::finished, this, [this]() { + if (!m_futureImageData) { + return; + } + m_imageData = m_futureImageData->future().result(); + postProcess(m_imageData); + m_futureImageData->deleteLater(); + m_futureImageData = nullptr; + + Q_EMIT paletteChanged(); + }); + m_futureImageData->setFuture(future); + }; + + if (!m_sourceItem || !m_sourceItem->window() || !m_sourceItem->window()->isVisible()) { + if (!m_sourceImage.isNull()) { + runUpdate(); + } else { + m_imageData = {}; + Q_EMIT paletteChanged(); + } + return; + } + + if (m_grabResult) { + disconnect(m_grabResult.data(), nullptr, this, nullptr); + m_grabResult.clear(); + } + + m_grabResult = m_sourceItem->grabToImage(QSize(128, 128)); + + if (m_grabResult) { + connect(m_grabResult.data(), &QQuickItemGrabResult::ready, this, [this, runUpdate]() { + m_sourceImage = m_grabResult->image(); + m_grabResult.clear(); + runUpdate(); + }); + } +} + +static inline int squareDistance(QRgb color1, QRgb color2) +{ + // https://en.wikipedia.org/wiki/Color_difference + // Using RGB distance for performance, as CIEDE2000 is too complicated + if (qRed(color1) - qRed(color2) < 128) { + return 2 * pow(qRed(color1) - qRed(color2), 2) // + + 4 * pow(qGreen(color1) - qGreen(color2), 2) // + + 3 * pow(qBlue(color1) - qBlue(color2), 2); + } else { + return 3 * pow(qRed(color1) - qRed(color2), 2) // + + 4 * pow(qGreen(color1) - qGreen(color2), 2) // + + 2 * pow(qBlue(color1) - qBlue(color2), 2); + } +} + +void ImageColors::positionColor(QRgb rgb, QList &clusters) +{ + for (auto &stat : clusters) { + if (squareDistance(rgb, stat.centroid) < s_minimumSquareDistance) { + stat.colors.append(rgb); + return; + } + } + + ImageData::colorStat stat; + stat.colors.append(rgb); + stat.centroid = rgb; + clusters << stat; +} + +void ImageColors::positionColorMP(const decltype(ImageData::m_samples) &samples, decltype(ImageData::m_clusters) &clusters, int numCore) +{ +#if HAVE_OpenMP + if (samples.size() < 65536 /* 256^2 */ || numCore < 2) { +#else + if (true) { +#endif + // Fall back to single thread + for (auto color : samples) { + positionColor(color, clusters); + } + return; + } +#if HAVE_OpenMP + // Split the whole samples into multiple parts + const int numSamplesPerThread = samples.size() / numCore; + std::vector tempClusters(numCore, decltype(ImageData::m_clusters){}); +#pragma omp parallel for + for (int i = 0; i < numCore; ++i) { + const auto beginIt = std::next(samples.begin(), numSamplesPerThread * i); + const auto endIt = i < numCore - 1 ? std::next(samples.begin(), numSamplesPerThread * (i + 1)) : samples.end(); + + for (auto it = beginIt; it != endIt; it = std::next(it)) { + positionColor(*it, tempClusters[omp_get_thread_num()]); + } + } // END omp parallel for + + // Restore clusters + // Don't use std::as_const as memory will grow significantly + for (const auto &clusterPart : tempClusters) { + clusters << clusterPart; + } + for (int i = 0; i < clusters.size() - 1; ++i) { + auto &clusterA = clusters[i]; + if (clusterA.colors.empty()) { + continue; // Already merged + } + for (int j = i + 1; j < clusters.size(); ++j) { + auto &clusterB = clusters[j]; + if (clusterB.colors.empty()) { + continue; // Already merged + } + if (squareDistance(clusterA.centroid, clusterB.centroid) < s_minimumSquareDistance) { + // Merge colors in clusterB into clusterA + clusterA.colors.append(clusterB.colors); + clusterB.colors.clear(); + } + } + } + + auto removeIt = std::remove_if(clusters.begin(), clusters.end(), [](const ImageData::colorStat &stat) { + return stat.colors.empty(); + }); + clusters.erase(removeIt, clusters.end()); +#endif +} + +ImageData ImageColors::generatePalette(const QImage &sourceImage) +{ + ImageData imageData; + + if (sourceImage.isNull() || sourceImage.width() == 0) { + return imageData; + } + + imageData.m_clusters.clear(); + imageData.m_samples.clear(); + +#if HAVE_OpenMP + static const int numCore = std::min(8, omp_get_num_procs()); + omp_set_num_threads(numCore); +#else + constexpr int numCore = 1; +#endif + int r = 0; + int g = 0; + int b = 0; + int c = 0; + +#pragma omp parallel for collapse(2) reduction(+ : r) reduction(+ : g) reduction(+ : b) reduction(+ : c) + for (int x = 0; x < sourceImage.width(); ++x) { + for (int y = 0; y < sourceImage.height(); ++y) { + const QColor sampleColor = sourceImage.pixelColor(x, y); + if (sampleColor.alpha() == 0) { + continue; + } + if (ColorUtils::chroma(sampleColor) < 20) { + continue; + } + QRgb rgb = sampleColor.rgb(); + ++c; + r += qRed(rgb); + g += qGreen(rgb); + b += qBlue(rgb); +#pragma omp critical + imageData.m_samples << rgb; + } + } // END omp parallel for + + if (imageData.m_samples.isEmpty()) { + return imageData; + } + + positionColorMP(imageData.m_samples, imageData.m_clusters, numCore); + + imageData.m_average = QColor(r / c, g / c, b / c, 255); + + for (int iteration = 0; iteration < 5; ++iteration) { +#pragma omp parallel for private(r, g, b, c) + for (int i = 0; i < imageData.m_clusters.size(); ++i) { + auto &stat = imageData.m_clusters[i]; + r = 0; + g = 0; + b = 0; + c = 0; + + for (auto color : std::as_const(stat.colors)) { + c++; + r += qRed(color); + g += qGreen(color); + b += qBlue(color); + } + r = r / c; + g = g / c; + b = b / c; + stat.centroid = qRgb(r, g, b); + stat.ratio = std::clamp(qreal(stat.colors.count()) / qreal(imageData.m_samples.count()), 0.0, 1.0); + stat.colors = QList({stat.centroid}); + } // END omp parallel for + + positionColorMP(imageData.m_samples, imageData.m_clusters, numCore); + } + + std::sort(imageData.m_clusters.begin(), imageData.m_clusters.end(), [](const ImageData::colorStat &a, const ImageData::colorStat &b) { + return getClusterScore(a) > getClusterScore(b); + }); + + // compress blocks that became too similar + auto sourceIt = imageData.m_clusters.end(); + // Use index instead of iterator, because QList::erase may invalidate iterator. + std::vector itemsToDelete; + while (sourceIt != imageData.m_clusters.begin()) { + sourceIt--; + for (auto destIt = imageData.m_clusters.begin(); destIt != imageData.m_clusters.end() && destIt != sourceIt; destIt++) { + if (squareDistance((*sourceIt).centroid, (*destIt).centroid) < s_minimumSquareDistance) { + const qreal ratio = (*sourceIt).ratio / (*destIt).ratio; + const int r = ratio * qreal(qRed((*sourceIt).centroid)) + (1 - ratio) * qreal(qRed((*destIt).centroid)); + const int g = ratio * qreal(qGreen((*sourceIt).centroid)) + (1 - ratio) * qreal(qGreen((*destIt).centroid)); + const int b = ratio * qreal(qBlue((*sourceIt).centroid)) + (1 - ratio) * qreal(qBlue((*destIt).centroid)); + (*destIt).ratio += (*sourceIt).ratio; + (*destIt).centroid = qRgb(r, g, b); + itemsToDelete.push_back(std::distance(imageData.m_clusters.begin(), sourceIt)); + break; + } + } + } + for (auto i : std::as_const(itemsToDelete)) { + imageData.m_clusters.removeAt(i); + } + + imageData.m_highlight = QColor(); + imageData.m_dominant = QColor(imageData.m_clusters.first().centroid); + imageData.m_closestToBlack = Qt::white; + imageData.m_closestToWhite = Qt::black; + + imageData.m_palette.clear(); + + bool first = true; + +#pragma omp parallel for ordered + for (int i = 0; i < imageData.m_clusters.size(); ++i) { + const auto &stat = imageData.m_clusters[i]; + const QColor color(stat.centroid); + + QColor contrast = QColor(255 - color.red(), 255 - color.green(), 255 - color.blue()); + contrast.setHsl(contrast.hslHue(), // + contrast.hslSaturation(), // + 128 + (128 - contrast.lightness())); + QColor tempContrast; + int minimumDistance = 4681800; // max distance: 4*3*2*3*255*255 + for (const auto &stat : std::as_const(imageData.m_clusters)) { + const int distance = squareDistance(contrast.rgb(), stat.centroid); + + if (distance < minimumDistance) { + tempContrast = QColor(stat.centroid); + minimumDistance = distance; + } + } + + if (imageData.m_clusters.size() <= 3) { + if (qGray(imageData.m_dominant.rgb()) < 120) { + contrast = QColor(230, 230, 230); + } else { + contrast = QColor(20, 20, 20); + } + // TODO: replace m_clusters.size() > 3 with entropy calculation + } else if (squareDistance(contrast.rgb(), tempContrast.rgb()) < s_minimumSquareDistance * 1.5) { + contrast = tempContrast; + } else { + contrast = tempContrast; + contrast.setHsl(contrast.hslHue(), + contrast.hslSaturation(), + contrast.lightness() > 128 ? qMin(contrast.lightness() + 20, 255) : qMax(0, contrast.lightness() - 20)); + } + +#pragma omp ordered + { // BEGIN omp ordered + if (first) { + imageData.m_dominantContrast = contrast; + imageData.m_dominant = color; + } + first = false; + + if (!imageData.m_highlight.isValid() || ColorUtils::chroma(color) > ColorUtils::chroma(imageData.m_highlight)) { + imageData.m_highlight = color; + } + + if (qGray(color.rgb()) > qGray(imageData.m_closestToWhite.rgb())) { + imageData.m_closestToWhite = color; + } + if (qGray(color.rgb()) < qGray(imageData.m_closestToBlack.rgb())) { + imageData.m_closestToBlack = color; + } + imageData.m_palette << PaletteSwatch(stat.ratio, color, contrast); + } // END omp ordered + } + + return imageData; +} + +double ImageColors::getClusterScore(const ImageData::colorStat &stat) +{ + return stat.ratio * ColorUtils::chroma(QColor(stat.centroid)); +} + +void ImageColors::postProcess(ImageData &imageData) const +{ + constexpr short unsigned WCAG_NON_TEXT_CONTRAST_RATIO = 3; + constexpr qreal WCAG_TEXT_CONTRAST_RATIO = 4.5; + + auto platformTheme = qmlAttachedPropertiesObject(this, false); + if (!platformTheme) { + return; + } + + const QColor backgroundColor = static_cast(platformTheme)->backgroundColor(); + const qreal backgroundLum = ColorUtils::luminance(backgroundColor); + qreal lowerLum, upperLum; + // 192 is from kcm_colors + if (qGray(backgroundColor.rgb()) < 192) { + // (lowerLum + 0.05) / (backgroundLum + 0.05) >= 3 + lowerLum = WCAG_NON_TEXT_CONTRAST_RATIO * (backgroundLum + 0.05) - 0.05; + upperLum = 0.95; + } else { + // For light themes, still prefer lighter colors + // (lowerLum + 0.05) / (textLum + 0.05) >= 4.5 + const QColor textColor = + static_cast(qmlAttachedPropertiesObject(this, true))->textColor(); + const qreal textLum = ColorUtils::luminance(textColor); + lowerLum = WCAG_TEXT_CONTRAST_RATIO * (textLum + 0.05) - 0.05; + upperLum = backgroundLum; + } + + auto adjustSaturation = [](QColor &color) { + // Adjust saturation to make the color more vibrant + if (color.hsvSaturationF() < 0.5) { + const qreal h = color.hsvHueF(); + const qreal v = color.valueF(); + color.setHsvF(h, 0.5, v); + } + }; + adjustSaturation(imageData.m_dominant); + adjustSaturation(imageData.m_highlight); + adjustSaturation(imageData.m_average); + + auto adjustLightness = [lowerLum, upperLum](QColor &color) { + short unsigned colorOperationCount = 0; + const qreal h = color.hslHueF(); + const qreal s = color.hslSaturationF(); + const qreal l = color.lightnessF(); + while (ColorUtils::luminance(color.rgb()) < lowerLum && colorOperationCount++ < 10) { + color.setHslF(h, s, std::min(1.0, l + colorOperationCount * 0.03)); + } + while (ColorUtils::luminance(color.rgb()) > upperLum && colorOperationCount++ < 10) { + color.setHslF(h, s, std::max(0.0, l - colorOperationCount * 0.03)); + } + }; + adjustLightness(imageData.m_dominant); + adjustLightness(imageData.m_highlight); + adjustLightness(imageData.m_average); +} + +QList ImageColors::palette() const +{ + if (m_futureImageData) { + qCWarning(KirigamiLog) << m_futureImageData->future().isFinished(); + } + return_fallback(m_fallbackPalette) return m_imageData.m_palette; +} + +ColorUtils::Brightness ImageColors::paletteBrightness() const +{ + /* clang-format off */ + return_fallback(m_fallbackPaletteBrightness) + + return qGray(m_imageData.m_dominant.rgb()) < 128 ? ColorUtils::Dark : ColorUtils::Light; + /* clang-format on */ +} + +QColor ImageColors::average() const +{ + /* clang-format off */ + return_fallback_finally(m_fallbackAverage, linkBackgroundColor) + + return m_imageData.m_average; + /* clang-format on */ +} + +QColor ImageColors::dominant() const +{ + /* clang-format off */ + return_fallback_finally(m_fallbackDominant, linkBackgroundColor) + + return m_imageData.m_dominant; + /* clang-format on */ +} + +QColor ImageColors::dominantContrast() const +{ + /* clang-format off */ + return_fallback_finally(m_fallbackDominantContrasting, linkBackgroundColor) + + return m_imageData.m_dominantContrast; + /* clang-format on */ +} + +QColor ImageColors::foreground() const +{ + /* clang-format off */ + return_fallback_finally(m_fallbackForeground, textColor) + + if (paletteBrightness() == ColorUtils::Dark) + { + if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) { + return QColor(230, 230, 230); + } + return m_imageData.m_closestToWhite; + } else { + if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) { + return QColor(20, 20, 20); + } + return m_imageData.m_closestToBlack; + } + /* clang-format on */ +} + +QColor ImageColors::background() const +{ + /* clang-format off */ + return_fallback_finally(m_fallbackBackground, backgroundColor) + + if (paletteBrightness() == ColorUtils::Dark) { + if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) { + return QColor(20, 20, 20); + } + return m_imageData.m_closestToBlack; + } else { + if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) { + return QColor(230, 230, 230); + } + return m_imageData.m_closestToWhite; + } + /* clang-format on */ +} + +QColor ImageColors::highlight() const +{ + /* clang-format off */ + return_fallback_finally(m_fallbackHighlight, linkColor) + + return m_imageData.m_highlight; + /* clang-format on */ +} + +QColor ImageColors::closestToWhite() const +{ + /* clang-format off */ + return_fallback(Qt::white) + if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) { + return QColor(230, 230, 230); + } + /* clang-format on */ + + return m_imageData.m_closestToWhite; +} + +QColor ImageColors::closestToBlack() const +{ + /* clang-format off */ + return_fallback(Qt::black) + if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) { + return QColor(20, 20, 20); + } + /* clang-format on */ + return m_imageData.m_closestToBlack; +} + +#include "moc_imagecolors.cpp" diff --git a/src/imagecolors.h b/src/imagecolors.h new file mode 100644 index 0000000..a42846a --- /dev/null +++ b/src/imagecolors.h @@ -0,0 +1,294 @@ +/* + * SPDX-FileCopyrightText: 2020 Marco Martin + * SPDX-FileCopyrightText: 2024 ivan tkachenko + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +struct PaletteSwatch { + Q_GADGET + QML_VALUE_TYPE(imageColorsPaletteSwatch) + + Q_PROPERTY(qreal ratio READ ratio FINAL) + Q_PROPERTY(QColor color READ color FINAL) + Q_PROPERTY(QColor contrastColor READ contrastColor FINAL) + +public: + explicit PaletteSwatch(); + explicit PaletteSwatch(qreal ratio, const QColor &color, const QColor &contrastColor); + + qreal ratio() const; + const QColor &color() const; + const QColor &contrastColor() const; + + bool operator==(const PaletteSwatch &other) const; + +private: + qreal m_ratio; + QColor m_color; + QColor m_contrastColor; +}; + +struct ImageData { + struct colorStat { + QList colors; + QRgb centroid = 0; + qreal ratio = 0; + }; + + struct colorSet { + QColor average; + QColor text; + QColor background; + QColor highlight; + }; + + QList m_samples; + QList m_clusters; + QList m_palette; + + bool m_darkPalette = true; + QColor m_dominant = Qt::transparent; + QColor m_dominantContrast; + QColor m_average; + QColor m_highlight; + + QColor m_closestToBlack; + QColor m_closestToWhite; +}; + +/** + * Extracts the dominant colors from an element or an image and exports it to a color palette. + */ +class ImageColors : public QObject +{ + Q_OBJECT + QML_ELEMENT + /** + * The source from which colors should be extracted from. + * + * `source` can be one of the following: + * * Item + * * QImage + * * QIcon + * * Icon name + * + * Note that an Item's color palette will only be extracted once unless you + * call `update()`, regardless of how the item hanges. + */ + Q_PROPERTY(QVariant source READ source WRITE setSource NOTIFY sourceChanged FINAL) + + /** + * A list of colors and related information about then. + * + * Each list item has the following properties: + * * `color`: The color of the list item. + * * `ratio`: How dominant the color is in the source image. + * * `contrastingColor`: The color from the source image that's closest to the inverse of `color`. + * + * The list is sorted by `ratio`; the first element is the most + * dominant color in the source image and the last element is the + * least dominant color of the image. + * + * \note K-means clustering is used to extract these colors; see https://en.wikipedia.org/wiki/K-means_clustering. + */ + Q_PROPERTY(QList palette READ palette NOTIFY paletteChanged FINAL) + + /** + * Information whether the palette is towards a light or dark color + * scheme, possible values are: + * * ColorUtils.Light + * * ColorUtils.Dark + */ + Q_PROPERTY(ColorUtils::Brightness paletteBrightness READ paletteBrightness NOTIFY paletteChanged FINAL) + + /** + * The average color of the source image. + */ + Q_PROPERTY(QColor average READ average NOTIFY paletteChanged FINAL) + + /** + * The dominant color of the source image. + * + * The dominant color of the image is the color of the largest + * cluster in the image. + * + * \sa https://en.wikipedia.org/wiki/K-means_clustering + */ + Q_PROPERTY(QColor dominant READ dominant NOTIFY paletteChanged FINAL) + + /** + * Suggested "contrasting" color to the dominant one. It's the color in the palette nearest to the negative of the dominant + */ + Q_PROPERTY(QColor dominantContrast READ dominantContrast NOTIFY paletteChanged FINAL) + + /** + * An accent color extracted from the source image. + * + * The accent color is the color cluster with the highest CIELAB + * chroma in the source image. + * + * \sa https://en.wikipedia.org/wiki/Colorfulness#Chroma + */ + Q_PROPERTY(QColor highlight READ highlight NOTIFY paletteChanged FINAL) + + /** + * A color suitable for rendering text and other foreground + * over the source image. + * + * On dark items, this will be the color closest to white in + * the image if it's light enough, or a bright gray otherwise. + * On light items, this will be the color closest to black in + * the image if it's dark enough, or a dark gray otherwise. + */ + Q_PROPERTY(QColor foreground READ foreground NOTIFY paletteChanged FINAL) + + /** + * A color suitable for rendering a background behind the + * source image. + * + * On dark items, this will be the color closest to black in the + * image if it's dark enough, or a dark gray otherwise. + * On light items, this will be the color closest to white + * in the image if it's light enough, or a bright gray otherwise. + */ + Q_PROPERTY(QColor background READ background NOTIFY paletteChanged FINAL) + + /** + * The lightest color of the source image. + */ + Q_PROPERTY(QColor closestToWhite READ closestToWhite NOTIFY paletteChanged FINAL) + + /** + * The darkest color of the source image. + */ + Q_PROPERTY(QColor closestToBlack READ closestToBlack NOTIFY paletteChanged FINAL) + + /** + * The value to return when palette is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(QList fallbackPalette MEMBER m_fallbackPalette NOTIFY fallbackPaletteChanged FINAL) + + /** + * The value to return when paletteBrightness is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(ColorUtils::Brightness fallbackPaletteBrightness MEMBER m_fallbackPaletteBrightness NOTIFY fallbackPaletteBrightnessChanged FINAL) + + /** + * The value to return when average is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(QColor fallbackAverage MEMBER m_fallbackAverage NOTIFY fallbackAverageChanged FINAL) + + /** + * The value to return when dominant is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(QColor fallbackDominant MEMBER m_fallbackDominant NOTIFY fallbackDominantChanged FINAL) + + /** + * The value to return when dominantContrasting is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(QColor fallbackDominantContrasting MEMBER m_fallbackDominantContrasting NOTIFY fallbackDominantContrastingChanged FINAL) + + /** + * The value to return when highlight is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(QColor fallbackHighlight MEMBER m_fallbackHighlight NOTIFY fallbackHighlightChanged FINAL) + + /** + * The value to return when foreground is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(QColor fallbackForeground MEMBER m_fallbackForeground NOTIFY fallbackForegroundChanged FINAL) + + /** + * The value to return when background is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(QColor fallbackBackground MEMBER m_fallbackBackground NOTIFY fallbackBackgroundChanged FINAL) + +public: + explicit ImageColors(QObject *parent = nullptr); + ~ImageColors() override; + + void setSource(const QVariant &source); + QVariant source() const; + + void setSourceImage(const QImage &image); + QImage sourceImage() const; + + void setSourceItem(QQuickItem *source); + QQuickItem *sourceItem() const; + + Q_INVOKABLE void update(); + + QList palette() const; + ColorUtils::Brightness paletteBrightness() const; + QColor average() const; + QColor dominant() const; + QColor dominantContrast() const; + QColor highlight() const; + QColor foreground() const; + QColor background() const; + QColor closestToWhite() const; + QColor closestToBlack() const; + +Q_SIGNALS: + void sourceChanged(); + void paletteChanged(); + void fallbackPaletteChanged(); + void fallbackPaletteBrightnessChanged(); + void fallbackAverageChanged(); + void fallbackDominantChanged(); + void fallbackDominantContrastingChanged(); + void fallbackHighlightChanged(); + void fallbackForegroundChanged(); + void fallbackBackgroundChanged(); + +private: + static inline void positionColor(QRgb rgb, QList &clusters); + static void positionColorMP(const decltype(ImageData::m_samples) &samples, decltype(ImageData::m_clusters) &clusters, int numCore = 0); + static ImageData generatePalette(const QImage &sourceImage); + + static double getClusterScore(const ImageData::colorStat &stat); + void postProcess(ImageData &imageData) const; + + // Arbitrary number that seems to work well + static const int s_minimumSquareDistance = 32000; + QPointer m_window; + QVariant m_source; + QPointer m_sourceItem; + QSharedPointer m_grabResult; + QImage m_sourceImage; + QFutureWatcher *m_futureSourceImageData = nullptr; + + QFutureWatcher *m_futureImageData = nullptr; + ImageData m_imageData; + + QList m_fallbackPalette; + ColorUtils::Brightness m_fallbackPaletteBrightness; + QColor m_fallbackAverage; + QColor m_fallbackDominant; + QColor m_fallbackDominantContrasting; + QColor m_fallbackHighlight; + QColor m_fallbackForeground; + QColor m_fallbackBackground; +}; diff --git a/src/kirigamiplugin.cpp b/src/kirigamiplugin.cpp new file mode 100644 index 0000000..e9beeac --- /dev/null +++ b/src/kirigamiplugin.cpp @@ -0,0 +1,181 @@ +/* + * SPDX-FileCopyrightText: 2009 Alan Alpert + * SPDX-FileCopyrightText: 2010 Ménard Alexis + * SPDX-FileCopyrightText: 2010 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "kirigamiplugin.h" + +#include +#if defined(Q_OS_ANDROID) +#include +#endif +#include +#include + +#include "platform/styleselector.h" + +#ifdef KIRIGAMI_BUILD_TYPE_STATIC +#include "loggingcategory.h" +#include +#endif + +// This is required for declarative registration to work on Windows. +// This is normally generated by Qt but since we need a manually written plugin +// file, we need to include this ourselves. +extern void qml_register_types_org_kde_kirigami(); +Q_GHS_KEEP_REFERENCE(qml_register_types_org_kde_kirigami) + +// we can't do this in the plugin object directly, as that can live in a different thread +// and event filters are only allowed in the same thread as the filtered object +class LanguageChangeEventFilter : public QObject +{ + Q_OBJECT +public: + bool eventFilter(QObject *receiver, QEvent *event) override + { + if (event->type() == QEvent::LanguageChange && receiver == QCoreApplication::instance()) { + Q_EMIT languageChangeEvent(); + } + return QObject::eventFilter(receiver, event); + } + +Q_SIGNALS: + void languageChangeEvent(); +}; + +KirigamiPlugin::KirigamiPlugin(QObject *parent) + : QQmlExtensionPlugin(parent) +{ + // See above. + volatile auto registration = &qml_register_types_org_kde_kirigami; + Q_UNUSED(registration) + + auto filter = new LanguageChangeEventFilter; + filter->moveToThread(QCoreApplication::instance()->thread()); + QCoreApplication::instance()->installEventFilter(filter); + connect(filter, &LanguageChangeEventFilter::languageChangeEvent, this, &KirigamiPlugin::languageChangeEvent); +} + +QUrl KirigamiPlugin::componentUrl(const QString &fileName) const +{ + return Kirigami::Platform::StyleSelector::componentUrl(fileName); +} + +void KirigamiPlugin::registerTypes(const char *uri) +{ +#if defined(Q_OS_ANDROID) + QResource::registerResource(QStringLiteral("assets:/android_rcc_bundle.rcc")); +#endif + + Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.kirigami")); + + Kirigami::Platform::StyleSelector::setBaseUrl(baseUrl()); + + if (QIcon::themeName().isEmpty() && !qEnvironmentVariableIsSet("XDG_CURRENT_DESKTOP")) { +#if defined(Q_OS_ANDROID) + QIcon::setThemeSearchPaths({QStringLiteral("assets:/qml/org/kde/kirigami"), QStringLiteral(":/icons")}); +#else + QIcon::setThemeSearchPaths({Kirigami::Platform::StyleSelector::resolveFilePath(QStringLiteral(".")), QStringLiteral(":/icons")}); +#endif + QIcon::setThemeName(QStringLiteral("breeze-internal")); + } else { + QIcon::setFallbackSearchPaths(QIcon::fallbackSearchPaths() << Kirigami::Platform::StyleSelector::resolveFilePath(QStringLiteral("icons"))); + } + + qmlRegisterType(componentUrl(QStringLiteral("Action.qml")), uri, 2, 0, "Action"); + qmlRegisterType(componentUrl(QStringLiteral("AbstractApplicationHeader.qml")), uri, 2, 0, "AbstractApplicationHeader"); + qmlRegisterType(componentUrl(QStringLiteral("AbstractApplicationWindow.qml")), uri, 2, 0, "AbstractApplicationWindow"); + qmlRegisterType(componentUrl(QStringLiteral("ApplicationWindow.qml")), uri, 2, 0, "ApplicationWindow"); + qmlRegisterType(componentUrl(QStringLiteral("OverlayDrawer.qml")), uri, 2, 0, "OverlayDrawer"); + qmlRegisterType(componentUrl(QStringLiteral("ContextDrawer.qml")), uri, 2, 0, "ContextDrawer"); + qmlRegisterType(componentUrl(QStringLiteral("GlobalDrawer.qml")), uri, 2, 0, "GlobalDrawer"); + qmlRegisterType(componentUrl(QStringLiteral("Heading.qml")), uri, 2, 0, "Heading"); + qmlRegisterType(componentUrl(QStringLiteral("PageRow.qml")), uri, 2, 0, "PageRow"); + + qmlRegisterType(componentUrl(QStringLiteral("OverlaySheet.qml")), uri, 2, 0, "OverlaySheet"); + qmlRegisterType(componentUrl(QStringLiteral("Page.qml")), uri, 2, 0, "Page"); + qmlRegisterType(componentUrl(QStringLiteral("ScrollablePage.qml")), uri, 2, 0, "ScrollablePage"); + qmlRegisterType(componentUrl(QStringLiteral("SwipeListItem.qml")), uri, 2, 0, "SwipeListItem"); + + // 2.1 + qmlRegisterType(componentUrl(QStringLiteral("AbstractApplicationItem.qml")), uri, 2, 1, "AbstractApplicationItem"); + qmlRegisterType(componentUrl(QStringLiteral("ApplicationItem.qml")), uri, 2, 1, "ApplicationItem"); + + // 2.4 + qmlRegisterType(componentUrl(QStringLiteral("AbstractCard.qml")), uri, 2, 4, "AbstractCard"); + qmlRegisterType(componentUrl(QStringLiteral("Card.qml")), uri, 2, 4, "Card"); + qmlRegisterType(componentUrl(QStringLiteral("CardsListView.qml")), uri, 2, 4, "CardsListView"); + qmlRegisterType(componentUrl(QStringLiteral("CardsLayout.qml")), uri, 2, 4, "CardsLayout"); + qmlRegisterType(componentUrl(QStringLiteral("InlineMessage.qml")), uri, 2, 4, "InlineMessage"); + + // 2.5 + qmlRegisterType(componentUrl(QStringLiteral("ListItemDragHandle.qml")), uri, 2, 5, "ListItemDragHandle"); + qmlRegisterType(componentUrl(QStringLiteral("ActionToolBar.qml")), uri, 2, 5, "ActionToolBar"); + + // 2.6 + qmlRegisterType(componentUrl(QStringLiteral("AboutPage.qml")), uri, 2, 6, "AboutPage"); + qmlRegisterType(componentUrl(QStringLiteral("LinkButton.qml")), uri, 2, 6, "LinkButton"); + qmlRegisterType(componentUrl(QStringLiteral("UrlButton.qml")), uri, 2, 6, "UrlButton"); + + // 2.7 + qmlRegisterType(componentUrl(QStringLiteral("ActionTextField.qml")), uri, 2, 7, "ActionTextField"); + + // 2.8 + qmlRegisterType(componentUrl(QStringLiteral("SearchField.qml")), uri, 2, 8, "SearchField"); + qmlRegisterType(componentUrl(QStringLiteral("PasswordField.qml")), uri, 2, 8, "PasswordField"); + + // 2.10 + qmlRegisterType(componentUrl(QStringLiteral("ListSectionHeader.qml")), uri, 2, 10, "ListSectionHeader"); + + // 2.11 + qmlRegisterType(componentUrl(QStringLiteral("PagePoolAction.qml")), uri, 2, 11, "PagePoolAction"); + + // 2.12 + qmlRegisterType(componentUrl(QStringLiteral("PlaceholderMessage.qml")), uri, 2, 12, "PlaceholderMessage"); + + // 2.14 + qmlRegisterType(componentUrl(QStringLiteral("FlexColumn.qml")), uri, 2, 14, "FlexColumn"); + + // 2.19 + qmlRegisterType(componentUrl(QStringLiteral("AboutItem.qml")), uri, 2, 19, "AboutItem"); + qmlRegisterType(componentUrl(QStringLiteral("NavigationTabBar.qml")), uri, 2, 19, "NavigationTabBar"); + qmlRegisterType(componentUrl(QStringLiteral("NavigationTabButton.qml")), uri, 2, 19, "NavigationTabButton"); + qmlRegisterType(componentUrl(QStringLiteral("Chip.qml")), uri, 2, 19, "Chip"); + qmlRegisterType(componentUrl(QStringLiteral("LoadingPlaceholder.qml")), uri, 2, 19, "LoadingPlaceholder"); + + // 2.20 + qmlRegisterType(componentUrl(QStringLiteral("SelectableLabel.qml")), uri, 2, 20, "SelectableLabel"); + qmlRegisterType(componentUrl(QStringLiteral("InlineViewHeader.qml")), uri, 2, 20, "InlineViewHeader"); + qmlRegisterType(componentUrl(QStringLiteral("ContextualHelpButton.qml")), uri, 2, 20, "ContextualHelpButton"); +} + +void KirigamiPlugin::initializeEngine(QQmlEngine *engine, const char *uri) +{ + Q_UNUSED(uri); + connect(this, &KirigamiPlugin::languageChangeEvent, engine, &QQmlEngine::retranslate); +} + +#ifdef KIRIGAMI_BUILD_TYPE_STATIC +KirigamiPlugin &KirigamiPlugin::getInstance() +{ + static KirigamiPlugin instance; + return instance; +} + +void KirigamiPlugin::registerTypes(QQmlEngine *engine) +{ + if (engine) { + engine->addImportPath(QLatin1String(":/")); + } else { + qCWarning(KirigamiLog) + << "Registering Kirigami on a null QQmlEngine instance - you likely want to pass a valid engine, or you will want to manually add the " + "qrc root path :/ to your import paths list so the engine is able to load the plugin"; + } +} +#endif + +#include "kirigamiplugin.moc" +#include "moc_kirigamiplugin.cpp" diff --git a/src/kirigamiplugin.h b/src/kirigamiplugin.h new file mode 100644 index 0000000..060e7ae --- /dev/null +++ b/src/kirigamiplugin.h @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2009 Alan Alpert + * SPDX-FileCopyrightText: 2010 Ménard Alexis + * SPDX-FileCopyrightText: 2010 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef KIRIGAMIPLUGIN_H +#define KIRIGAMIPLUGIN_H + +#include +#include +#include + +class KirigamiPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + KirigamiPlugin(QObject *parent = nullptr); + void registerTypes(const char *uri) override; + void initializeEngine(QQmlEngine *engine, const char *uri) override; + +#ifdef KIRIGAMI_BUILD_TYPE_STATIC + static KirigamiPlugin &getInstance(); + static void registerTypes(QQmlEngine *engine = nullptr); +#endif + +Q_SIGNALS: + void languageChangeEvent(); + +private: + QUrl componentUrl(const QString &fileName) const; +}; + +#endif diff --git a/src/layouts/CMakeLists.txt b/src/layouts/CMakeLists.txt new file mode 100644 index 0000000..6d5d2ba --- /dev/null +++ b/src/layouts/CMakeLists.txt @@ -0,0 +1,54 @@ + +add_library(KirigamiLayouts) +ecm_add_qml_module(KirigamiLayouts URI "org.kde.kirigami.layouts" + VERSION 2.0 + GENERATE_PLUGIN_SOURCE + INSTALLED_PLUGIN_TARGET KF6KirigamiLayoutsplugin + DEPENDENCIES QtQuick org.kde.kirigami.platform +) + +ecm_qt_declare_logging_category(KirigamiLayouts + HEADER loggingcategory.h + IDENTIFIER KirigamiLayoutsLog + CATEGORY_NAME kf.kirigami.layouts + DESCRIPTION "KirigamiLayouts" + DEFAULT_SEVERITY Warning + EXPORT KIRIGAMI +) + +target_sources(KirigamiLayouts PRIVATE + columnview.cpp + displayhint.cpp + formlayoutattached.cpp + headerfooterlayout.cpp + padding.cpp + sizegroup.cpp + toolbarlayout.cpp + toolbarlayoutdelegate.cpp + pagestackattached.cpp + pagestackattached.h +) + +ecm_target_qml_sources(KirigamiLayouts SOURCES + FormLayout.qml +) + +set_target_properties(KirigamiLayouts PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION 6 + EXPORT_NAME "KirigamiLayouts" +) + +target_include_directories(KirigamiLayouts PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) + +target_link_libraries(KirigamiLayouts PRIVATE Qt6::Quick Qt6::QuickControls2 KirigamiPlatform) + +if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + set(_extra_options DEBUGINFO) +else() + set(_extra_options PRECOMPILE OPTIMIZED) +endif() + +ecm_finalize_qml_module(KirigamiLayouts EXPORT KirigamiTargets) + +install(TARGETS KirigamiLayouts EXPORT KirigamiTargets ${KF_INSTALL_DEFAULT_ARGUMENTS}) diff --git a/src/layouts/FormLayout.qml b/src/layouts/FormLayout.qml new file mode 100644 index 0000000..e8b73d2 --- /dev/null +++ b/src/layouts/FormLayout.qml @@ -0,0 +1,492 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * SPDX-FileCopyrightText: 2022 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import QtQuick.Templates as T +import org.kde.kirigami as Kirigami + +/** + * This is the base class for Form layouts conforming to the + * Kirigami Human Interface Guidelines. The layout consists + * of two columns: the left column contains only right-aligned + * labels provided by a Kirigami.FormData attached property, + * the right column contains left-aligned child types. + * + * Child types can be sectioned using an QtQuick.Item + * or Kirigami.Separator with a Kirigami.FormData + * attached property, see FormLayoutAttached::isSection for details. + * + * Example usage: + * @code + * import QtQuick.Controls as QQC2 + * import org.kde.kirigami as Kirigami + * + * Kirigami.FormLayout { + * QQC2.TextField { + * Kirigami.FormData.label: "Label:" + * } + * Kirigami.Separator { + * Kirigami.FormData.label: "Section Title" + * Kirigami.FormData.isSection: true + * } + * QQC2.TextField { + * Kirigami.FormData.label: "Label:" + * } + * QQC2.TextField { + * } + * } + * @endcode + * @see FormLayoutAttached + * @since 2.3 + * @inherit QtQuick.Item + */ +Item { + id: root + + /** + * @brief This property tells whether the form layout is in wide mode. + * + * If true, the layout will be optimized for a wide screen, such as + * a desktop machine (the labels will be on a left column, + * the fields on a right column beside it), if false (such as on a phone) + * everything is laid out in a single column. + * + * By default this property automatically adjusts the layout + * if there is enough screen space. + * + * Set this to true for a convergent design, + * set this to false for a mobile-only design. + */ + property bool wideMode: width >= lay.wideImplicitWidth + + /** + * If for some implementation reason multiple FormLayouts have to appear + * on the same page, they can have each other in twinFormLayouts, + * so they will vertically align with each other perfectly + * + * @since 5.53 + */ + property list twinFormLayouts // should be list but we can't have a recursive declaration + + onTwinFormLayoutsChanged: { + for (const twinFormLayout of twinFormLayouts) { + if (!(root in twinFormLayout.children[0].reverseTwins)) { + twinFormLayout.children[0].reverseTwins.push(root) + Qt.callLater(() => twinFormLayout.children[0].reverseTwinsChanged()); + } + } + } + + property Kirigami.ScrollablePage scrollablePage: findAncestor(root, (item) => item instanceof Kirigami.ScrollablePage) + + function findAncestor(item: Item, predicate: /*function Item => bool*/ var): Item { + let target = item.parent + while (target && !predicate(target)) { + target = target.parent + } + return target + } + + function ensureVisible(item: Item): void { + if (item && root.scrollablePage) { + const itemPosition = scrollablePage.flickable.contentItem.mapFromItem(item, 0, 0) + root.scrollablePage.ensureVisible(item, itemPosition.x - item.x, itemPosition.y - item.y) + } + } + + Connections { + target: root.Window + enabled: root.scrollablePage + function onActiveFocusItemChanged(): void { + if (root.Window.activeFocusItem && findAncestor(root.Window.activeFocusItem, (item) => item === root)) { + root.ensureVisible(root.Window.activeFocusItem) + } + } + } + + Component.onCompleted: { + relayoutTimer.triggered(); + } + + Component.onDestruction: { + for (const twinFormLayout of twinFormLayouts) { + const child = twinFormLayout.children[0]; + child.reverseTwins = child.reverseTwins.filter(value => value !== root); + } + } + + implicitWidth: lay.wideImplicitWidth + implicitHeight: lay.implicitHeight + Layout.preferredHeight: lay.implicitHeight + Layout.fillWidth: true + Accessible.role: Accessible.Form + + GridLayout { + id: lay + property int wideImplicitWidth + columns: root.wideMode ? 2 : 1 + rowSpacing: Kirigami.Units.smallSpacing + columnSpacing: Kirigami.Units.largeSpacing + + //TODO: use state machine + Binding { + when: !root.wideMode + target: lay + property: "width" + value: root.width + restoreMode: Binding.RestoreBinding + } + Binding { + when: root.wideMode + target: lay + property: "width" + value: root.implicitWidth + restoreMode: Binding.RestoreBinding + } + anchors { + horizontalCenter: root.wideMode ? root.horizontalCenter : undefined + left: root.wideMode ? undefined : root.left + } + + property var reverseTwins: [] + property var knownItems: [] + property var buddies: [] + property int knownItemsImplicitWidth: { + let hint = 0; + for (const item of knownItems) { + if (typeof item.Layout === "undefined") { + // Items may have been dynamically destroyed. Even + // printing such zombie wrappers results in a + // meaningless "TypeError: Type error". Normally they + // should be cleaned up from the array, but it would + // trigger a binding loop if done here. + // + // This is, so far, the only way to detect them. + continue; + } + const actualWidth = item.Layout.preferredWidth > 0 + ? item.Layout.preferredWidth + : item.implicitWidth; + + hint = Math.max(hint, item.Layout.minimumWidth, Math.min(actualWidth, item.Layout.maximumWidth)); + } + return hint; + } + property int buddiesImplicitWidth: { + let hint = 0; + + for (const buddy of buddies) { + if (buddy.visible && buddy.item !== null && !buddy.item.Kirigami.FormData.isSection) { + hint = Math.max(hint, buddy.implicitWidth); + } + } + return hint; + } + readonly property var actualTwinFormLayouts: { + // We need to copy that array by value + const list = lay.reverseTwins.slice(); + for (const parentLay of root.twinFormLayouts) { + if (!parentLay || !parentLay.hasOwnProperty("children")) { + continue; + } + list.push(parentLay); + for (const childLay of parentLay.children[0].reverseTwins) { + if (childLay && !(childLay in list)) { + list.push(childLay); + } + } + } + return list; + } + + Timer { + id: hintCompression + interval: 0 + onTriggered: { + if (root.wideMode) { + lay.wideImplicitWidth = lay.implicitWidth; + } + } + } + onImplicitWidthChanged: hintCompression.restart(); + //This invisible row is used to sync alignment between multiple layouts + + Item { + Layout.preferredWidth: { + let hint = lay.buddiesImplicitWidth; + for (const item of lay.actualTwinFormLayouts) { + if (item && item.hasOwnProperty("children")) { + hint = Math.max(hint, item.children[0].buddiesImplicitWidth); + } + } + return hint; + } + Layout.preferredHeight: 2 + } + Item { + Layout.preferredWidth: { + let hint = Math.min(root.width, lay.knownItemsImplicitWidth); + for (const item of lay.actualTwinFormLayouts) { + if (item.hasOwnProperty("children")) { + hint = Math.max(hint, item.children[0].knownItemsImplicitWidth); + } + } + return hint; + } + Layout.preferredHeight: 2 + } + } + + Item { + id: temp + + /** + * The following two functions are used in the label buddy items. + * + * They're in this mostly unused item to keep them private to the FormLayout + * without creating another QObject. + * + * Normally, such complex things in bindings are kinda bad for performance + * but this is a fairly static property. If for some reason an application + * decides to obsessively change its alignment, V8's JIT hotspot optimisations + * will kick in. + */ + + /** + * @param {Item} item + * @returns {Qt::Alignment} + */ + function effectiveLayout(item: Item): /*Qt.Alignment*/ int { + if (!item) { + return 0; + } + const verticalAlignment = + item.Kirigami.FormData.labelAlignment !== 0 + ? item.Kirigami.FormData.labelAlignment + : Qt.AlignTop; + + if (item.Kirigami.FormData.isSection) { + return Qt.AlignHCenter; + } + if (root.wideMode) { + return Qt.AlignRight | verticalAlignment; + } + return Qt.AlignLeft | Qt.AlignBottom; + } + + /** + * @param {Item} item + * @returns vertical alignment of the item passed as an argument. + */ + function effectiveTextLayout(item: Item): /*Qt.Alignment*/ int { + if (!item) { + return 0; + } + if (root.wideMode && !item.Kirigami.FormData.isSection) { + return item.Kirigami.FormData.labelAlignment !== 0 ? item.Kirigami.FormData.labelAlignment : Text.AlignVCenter; + } + return Text.AlignBottom; + } + } + + Timer { + id: relayoutTimer + interval: 0 + onTriggered: { + const __items = root.children; + // exclude the layout and temp + for (let i = 2; i < __items.length; ++i) { + const item = __items[i]; + + // skip items that are already there + if (lay.knownItems.indexOf(item) !== -1 || item instanceof Repeater) { + continue; + } + lay.knownItems.push(item); + + const itemContainer = itemComponent.createObject(temp, { item }); + + // if it's a labeled section header, add extra spacing before it + if (item.Kirigami.FormData.label.length > 0 && item.Kirigami.FormData.isSection) { + placeHolderComponent.createObject(lay, { item }); + } + + const buddy = buddyComponent.createObject(lay, { item, index: i - 2 }); + + itemContainer.parent = lay; + lay.buddies.push(buddy); + } + lay.knownItemsChanged(); + lay.buddiesChanged(); + hintCompression.triggered(); + } + } + + onChildrenChanged: relayoutTimer.restart(); + + Component { + id: itemComponent + Item { + id: container + + property Item item + + enabled: item?.enabled ?? false + visible: item?.visible ?? false + + // NOTE: work around a GridLayout quirk which doesn't lay out items with null size hints causing things to be laid out incorrectly in some cases + implicitWidth: item !== null ? Math.max(item.implicitWidth, 1) : 0 + implicitHeight: item !== null ? Math.max(item.implicitHeight, 1) : 0 + Layout.preferredWidth: item !== null ? Math.max(1, item.Layout.preferredWidth > 0 ? item.Layout.preferredWidth : Math.ceil(item.implicitWidth)) : 0 + Layout.preferredHeight: item !== null ? Math.max(1, item.Layout.preferredHeight > 0 ? item.Layout.preferredHeight : Math.ceil(item.implicitHeight)) : 0 + + Layout.minimumWidth: item?.Layout.minimumWidth ?? 0 + Layout.minimumHeight: item?.Layout.minimumHeight ?? 0 + + Layout.maximumWidth: item?.Layout.maximumWidth ?? 0 + Layout.maximumHeight: item?.Layout.maximumHeight ?? 0 + + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillWidth: item !== null && (item instanceof TextInput || item.Layout.fillWidth || item.Kirigami.FormData.isSection) + Layout.columnSpan: item?.Kirigami.FormData.isSection ? lay.columns : 1 + onItemChanged: { + if (!item) { + container.destroy(); + } + } + onXChanged: if (item !== null) { item.x = x + lay.x; } + // Assume lay.y is always 0 + onYChanged: if (item !== null) { item.y = y + lay.y; } + onWidthChanged: if (item !== null) { item.width = width; } + Component.onCompleted: item.x = x + lay.x; + Connections { + target: lay + function onXChanged(): void { + if (container.item !== null) { + container.item.x = container.x + lay.x; + } + } + } + } + } + Component { + id: placeHolderComponent + Item { + property Item item + + enabled: item?.enabled ?? false + visible: item?.visible ?? false + + width: Kirigami.Units.smallSpacing + height: Kirigami.Units.smallSpacing + Layout.topMargin: item?.height > 0 ? Kirigami.Units.smallSpacing : 0 + onItemChanged: { + if (!item) { + destroy(); + } + } + } + } + Component { + id: buddyComponent + Kirigami.Heading { + id: labelItem + + property Item item + property int index + + enabled: { + const buddy = item?.Kirigami.FormData.buddyFor; + if (buddy) { + return buddy.enabled; + } else { + return item?.enabled ?? false; + } + } + visible: (item?.visible && (root.wideMode || text.length > 0)) ?? false + Kirigami.MnemonicData.enabled: { + const buddy = item?.Kirigami.FormData.buddyFor; + if (buddy && buddy.enabled && buddy.visible && buddy.activeFocusOnTab) { + // Only set mnemonic if the buddy doesn't already have one. + const buddyMnemonic = buddy.Kirigami.MnemonicData; + return !buddyMnemonic.label || !buddyMnemonic.enabled; + } else { + return false; + } + } + Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.FormLabel + Kirigami.MnemonicData.label: item?.Kirigami.FormData.label ?? "" + text: Kirigami.MnemonicData.richTextLabel + Accessible.name: Kirigami.MnemonicData.plainTextLabel + type: item?.Kirigami.FormData.isSection ? Kirigami.Heading.Type.Primary : Kirigami.Heading.Type.Normal + + level: item?.Kirigami.FormData.isSection ? 3 : 5 + + Layout.columnSpan: item?.Kirigami.FormData.isSection ? lay.columns : 1 + Layout.preferredHeight: { + if (!item) { + return 0; + } + if (item.Kirigami.FormData.label.length > 0) { + // Add extra whitespace before textual section headers, which + // looks better than separator lines + if (item.Kirigami.FormData.isSection && labelItem.index !== 0) { + return implicitHeight + Kirigami.Units.largeSpacing * 2; + } + else if (root.wideMode && !(item.Kirigami.FormData.buddyFor instanceof TextEdit)) { + return Math.max(implicitHeight, item.Kirigami.FormData.buddyFor.height) + } + return implicitHeight; + } + return Kirigami.Units.smallSpacing; + } + + Layout.alignment: temp.effectiveLayout(item) + verticalAlignment: temp.effectiveTextLayout(item) + + Layout.fillWidth: !root.wideMode + wrapMode: Text.Wrap + + Layout.topMargin: { + if (!item) { + return 0; + } + if (root.wideMode && item.Kirigami.FormData.buddyFor.parent !== root) { + return item.Kirigami.FormData.buddyFor.y; + } + if (index === 0 || root.wideMode) { + return 0; + } + return Kirigami.Units.largeSpacing * 2; + } + onItemChanged: { + if (!item) { + destroy(); + } + } + Shortcut { + sequence: labelItem.Kirigami.MnemonicData.sequence + onActivated: { + const buddy = labelItem.item.Kirigami.FormData.buddyFor; + + const buttonBuddy = buddy as T.AbstractButton; + // animateClick is only in Qt 6.8, + // it also takes into account focus policy. + if (buttonBuddy && buttonBuddy.animateClick) { + buttonBuddy.animateClick(); + } else { + buddy.forceActiveFocus(Qt.ShortcutFocusReason); + } + } + } + } + } +} diff --git a/src/layouts/columnview.cpp b/src/layouts/columnview.cpp new file mode 100644 index 0000000..da151d4 --- /dev/null +++ b/src/layouts/columnview.cpp @@ -0,0 +1,2004 @@ +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "columnview.h" +#include "columnview_p.h" + +#include "loggingcategory.h" +#include +#include +#include +#include +#include +#include +#include + +#include "platform/units.h" + +class QmlComponentsPoolSingleton +{ +public: + QmlComponentsPoolSingleton() + { + } + static QmlComponentsPool *instance(QQmlEngine *engine); + +private: + QHash m_instances; +}; + +Q_GLOBAL_STATIC(QmlComponentsPoolSingleton, privateQmlComponentsPoolSelf) + +QmlComponentsPool *QmlComponentsPoolSingleton::instance(QQmlEngine *engine) +{ + Q_ASSERT(engine); + auto componentPool = privateQmlComponentsPoolSelf->m_instances.value(engine); + + if (componentPool) { + return componentPool; + } + + componentPool = new QmlComponentsPool(engine); + + const auto removePool = [engine]() { + // NB: do not derefence engine. it may be dangling already! + if (privateQmlComponentsPoolSelf) { + privateQmlComponentsPoolSelf->m_instances.remove(engine); + } + }; + QObject::connect(engine, &QObject::destroyed, engine, removePool); + QObject::connect(componentPool, &QObject::destroyed, componentPool, removePool); + + privateQmlComponentsPoolSelf->m_instances[engine] = componentPool; + return componentPool; +} + +QmlComponentsPool::QmlComponentsPool(QQmlEngine *engine) + : QObject(engine) +{ + QQmlComponent component(engine); + + /* clang-format off */ + component.setData(QByteArrayLiteral(R"( +import QtQuick +import org.kde.kirigami as Kirigami + +QtObject { + readonly property Component leadingSeparator: Kirigami.Separator { + property Item column + property bool inToolBar + property Kirigami.ColumnView view + + // positioning trick to hide the very first separator + visible: { + if (!view || !view.separatorVisible) { + return false; + } + + return view && (LayoutMirroring.enabled + ? view.contentX + view.width > column.x + column.width + : view.contentX < column.x); + } + + anchors.top: column.top + anchors.left: column.left + anchors.bottom: column.bottom + anchors.topMargin: inToolBar ? Kirigami.Units.largeSpacing : 0 + anchors.bottomMargin: inToolBar ? Kirigami.Units.largeSpacing : 0 + Kirigami.Theme.colorSet: Kirigami.Theme.Header + Kirigami.Theme.inherit: false + } + + readonly property Component trailingSeparator: Kirigami.Separator { + property Item column + + anchors.top: column.top + anchors.right: column.right + anchors.bottom: column.bottom + Kirigami.Theme.colorSet: Kirigami.Theme.Header + Kirigami.Theme.inherit: false + } +} +)"), QUrl(QStringLiteral("columnview.cpp"))); + /* clang-format on */ + + m_instance = component.create(); + // qCWarning(KirigamiLayoutsLog)<setParent(this); + + m_leadingSeparatorComponent = m_instance->property("leadingSeparator").value(); + Q_ASSERT(m_leadingSeparatorComponent); + + m_trailingSeparatorComponent = m_instance->property("trailingSeparator").value(); + Q_ASSERT(m_trailingSeparatorComponent); + + m_units = engine->singletonInstance("org.kde.kirigami.platform", "Units"); + Q_ASSERT(m_units); + + connect(m_units, &Kirigami::Platform::Units::gridUnitChanged, this, &QmlComponentsPool::gridUnitChanged); + connect(m_units, &Kirigami::Platform::Units::longDurationChanged, this, &QmlComponentsPool::longDurationChanged); +} + +QmlComponentsPool::~QmlComponentsPool() +{ +} + +///////// + +ColumnViewAttached::ColumnViewAttached(QObject *parent) + : QObject(parent) +{ +} + +ColumnViewAttached::~ColumnViewAttached() +{ +} + +void ColumnViewAttached::setIndex(int index) +{ + if (!m_customFillWidth && m_view) { + const bool oldFillWidth = m_fillWidth; + m_fillWidth = index == m_view->count() - 1; + if (oldFillWidth != m_fillWidth) { + Q_EMIT fillWidthChanged(); + } + } + + if (index == m_index) { + return; + } + + m_index = index; + Q_EMIT indexChanged(); +} + +int ColumnViewAttached::index() const +{ + return m_index; +} + +void ColumnViewAttached::setFillWidth(bool fill) +{ + if (m_view) { + disconnect(m_view.data(), &ColumnView::countChanged, this, nullptr); + } + m_customFillWidth = true; + + if (fill == m_fillWidth) { + return; + } + + m_fillWidth = fill; + Q_EMIT fillWidthChanged(); + + if (m_view) { + m_view->polish(); + } +} + +bool ColumnViewAttached::fillWidth() const +{ + return m_fillWidth; +} + +qreal ColumnViewAttached::reservedSpace() const +{ + return m_reservedSpace; +} + +void ColumnViewAttached::setReservedSpace(qreal space) +{ + if (m_view) { + disconnect(m_view.data(), &ColumnView::columnWidthChanged, this, nullptr); + } + m_customReservedSpace = true; + + if (qFuzzyCompare(space, m_reservedSpace)) { + return; + } + + m_reservedSpace = space; + Q_EMIT reservedSpaceChanged(); + + if (m_view) { + m_view->polish(); + } +} + +ColumnView *ColumnViewAttached::view() +{ + return m_view; +} + +void ColumnViewAttached::setView(ColumnView *view) +{ + if (view == m_view) { + return; + } + + if (m_view) { + disconnect(m_view.data(), nullptr, this, nullptr); + } + m_view = view; + + if (!m_customFillWidth && m_view) { + m_fillWidth = m_index == m_view->count() - 1; + connect(m_view.data(), &ColumnView::countChanged, this, [this]() { + m_fillWidth = m_index == m_view->count() - 1; + Q_EMIT fillWidthChanged(); + }); + } + if (!m_customReservedSpace && m_view) { + m_reservedSpace = m_view->columnWidth(); + connect(m_view.data(), &ColumnView::columnWidthChanged, this, [this]() { + m_reservedSpace = m_view->columnWidth(); + Q_EMIT reservedSpaceChanged(); + }); + } + + Q_EMIT viewChanged(); +} + +QQuickItem *ColumnViewAttached::originalParent() const +{ + return m_originalParent; +} + +void ColumnViewAttached::setOriginalParent(QQuickItem *parent) +{ + m_originalParent = parent; +} + +bool ColumnViewAttached::shouldDeleteOnRemove() const +{ + return m_shouldDeleteOnRemove; +} + +void ColumnViewAttached::setShouldDeleteOnRemove(bool del) +{ + m_shouldDeleteOnRemove = del; +} + +bool ColumnViewAttached::preventStealing() const +{ + return m_preventStealing; +} + +void ColumnViewAttached::setPreventStealing(bool prevent) +{ + if (prevent == m_preventStealing) { + return; + } + + m_preventStealing = prevent; + Q_EMIT preventStealingChanged(); +} + +bool ColumnViewAttached::isPinned() const +{ + return m_pinned; +} + +void ColumnViewAttached::setPinned(bool pinned) +{ + if (pinned == m_pinned) { + return; + } + + m_pinned = pinned; + + Q_EMIT pinnedChanged(); + + if (m_view) { + m_view->polish(); + } +} + +bool ColumnViewAttached::inViewport() const +{ + return m_inViewport; +} + +void ColumnViewAttached::setInViewport(bool inViewport) +{ + if (m_inViewport == inViewport) { + return; + } + + m_inViewport = inViewport; + + Q_EMIT inViewportChanged(); +} + +QQuickItem *ColumnViewAttached::globalHeader() const +{ + return m_globalHeader; +} + +void ColumnViewAttached::setGlobalHeader(QQuickItem *header) +{ + if (header == m_globalHeader) { + return; + } + + QQuickItem *oldHeader = m_globalHeader; + if (m_globalHeader) { + disconnect(m_globalHeader, nullptr, this, nullptr); + } + + m_globalHeader = header; + + connect(header, &QObject::destroyed, this, [this, header]() { + globalHeaderChanged(header, nullptr); + }); + + Q_EMIT globalHeaderChanged(oldHeader, header); +} + +QQuickItem *ColumnViewAttached::globalFooter() const +{ + return m_globalFooter; +} + +void ColumnViewAttached::setGlobalFooter(QQuickItem *footer) +{ + if (footer == m_globalFooter) { + return; + } + + QQuickItem *oldFooter = m_globalFooter; + if (m_globalFooter) { + disconnect(m_globalFooter, nullptr, this, nullptr); + } + + m_globalFooter = footer; + + connect(footer, &QObject::destroyed, this, [this, footer]() { + globalFooterChanged(footer, nullptr); + }); + + Q_EMIT globalFooterChanged(oldFooter, footer); +} + +///////// + +ContentItem::ContentItem(ColumnView *parent) + : QQuickItem(parent) + , m_view(parent) +{ + m_globalHeaderParent = new QQuickItem(this); + m_globalFooterParent = new QQuickItem(this); + + setFlags(flags() | ItemIsFocusScope); + m_slideAnim = new QPropertyAnimation(this); + m_slideAnim->setTargetObject(this); + m_slideAnim->setPropertyName("x"); + // NOTE: the duration will be taken from kirigami units upon classBegin + m_slideAnim->setDuration(0); + m_slideAnim->setEasingCurve(QEasingCurve(QEasingCurve::OutExpo)); + connect(m_slideAnim, &QPropertyAnimation::finished, this, [this]() { + if (!m_view->currentItem()) { + m_view->setCurrentIndex(m_items.indexOf(m_viewAnchorItem)); + } else { + QRectF mapped = m_view->currentItem()->mapRectToItem(m_view, QRectF(QPointF(0, 0), m_view->currentItem()->size())); + if (!QRectF(QPointF(0, 0), m_view->size()).intersects(mapped)) { + m_view->setCurrentIndex(m_items.indexOf(m_viewAnchorItem)); + } + } + }); + + connect(this, &QQuickItem::xChanged, this, &ContentItem::layoutPinnedItems); + m_creationInProgress = false; +} + +ContentItem::~ContentItem() +{ +} + +void ContentItem::setBoundedX(qreal x) +{ + if (!parentItem()) { + return; + } + m_slideAnim->stop(); + setX(qRound(qBound(qMin(0.0, -width() + parentItem()->width()), x, 0.0))); +} + +void ContentItem::animateX(qreal newX) +{ + if (!parentItem()) { + return; + } + + const qreal to = qRound(qBound(qMin(0.0, -width() + parentItem()->width()), newX, 0.0)); + + m_slideAnim->stop(); + m_slideAnim->setStartValue(x()); + m_slideAnim->setEndValue(to); + m_slideAnim->start(); +} + +void ContentItem::snapToItem() +{ + QQuickItem *firstItem = childAt(viewportLeft(), height() / 2); + if (!firstItem) { + return; + } + QQuickItem *nextItem = childAt(firstItem->x() + firstItem->width() + 1, height() / 2); + + // need to make the last item visible? + if (nextItem && // + ((m_view->dragging() && m_lastDragDelta < 0) // + || (!m_view->dragging() // + && (width() - viewportRight()) < (viewportLeft() - firstItem->x())))) { + m_viewAnchorItem = nextItem; + animateX(-nextItem->x() + m_leftPinnedSpace); + + // The first one found? + } else if ((m_view->dragging() && m_lastDragDelta >= 0) // + || (!m_view->dragging() && (viewportLeft() <= (firstItem->x() + (firstItem->width() / 2)))) // + || !nextItem) { + m_viewAnchorItem = firstItem; + animateX(-firstItem->x() + m_leftPinnedSpace); + + // the second? + } else { + m_viewAnchorItem = nextItem; + animateX(-nextItem->x() + m_leftPinnedSpace); + } +} + +qreal ContentItem::viewportLeft() const +{ + return -x() + m_leftPinnedSpace; +} + +qreal ContentItem::viewportRight() const +{ + return -x() + m_view->width() - m_rightPinnedSpace; +} + +qreal ContentItem::childWidth(QQuickItem *child) +{ + if (!parentItem()) { + return 0.0; + } + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(child, true)); + + if (m_columnResizeMode == ColumnView::SingleColumn) { + return qRound(parentItem()->width()); + + } else if (attached->fillWidth()) { + if (m_view->count() == 1) { + // single column + return qRound(parentItem()->width()); + } + + return qRound(qBound(m_columnWidth, (parentItem()->width() - attached->reservedSpace()), std::max(m_columnWidth, parentItem()->width()))); + + } else if (m_columnResizeMode == ColumnView::FixedColumns) { + return qRound(qMin(parentItem()->width(), m_columnWidth)); + + // DynamicColumns + } else { + // TODO:look for Layout size hints + qreal width = child->implicitWidth(); + + if (width < 1.0) { + width = m_columnWidth; + } + + return qRound(qMin(m_view->width(), width)); + } +} + +void ContentItem::layoutItems() +{ + setY(m_view->topPadding()); + setHeight(m_view->height() - m_view->topPadding() - m_view->bottomPadding()); + + qreal implicitWidth = 0; + qreal implicitHeight = 0; + qreal partialWidth = 0; + int i = 0; + m_leftPinnedSpace = 0; + m_rightPinnedSpace = 0; + + bool reverse = qApp->layoutDirection() == Qt::RightToLeft; + auto it = !reverse ? m_items.begin() : m_items.end(); + int increment = reverse ? -1 : +1; + auto lastPos = reverse ? m_items.begin() : m_items.end(); + + for (; it != lastPos; it += increment) { + // for (QQuickItem *child : std::as_const(m_items)) { + QQuickItem *child = reverse ? *(it - 1) : *it; + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(child, true)); + if (child == m_globalHeaderParent || child == m_globalFooterParent) { + continue; + } + + if (child->isVisible()) { + if (attached->isPinned() && m_view->columnResizeMode() != ColumnView::SingleColumn) { + QQuickItem *sep = nullptr; + int sepWidth = 0; + if (m_view->separatorVisible()) { + sep = ensureTrailingSeparator(child); + sepWidth = (sep ? sep->width() : 0); + } + const qreal width = childWidth(child); + const qreal widthDiff = std::max(0.0, m_view->width() - child->width()); // it's possible for the view width to be smaller than the child width + const qreal pageX = std::clamp(partialWidth, -x(), -x() + widthDiff); + qreal headerHeight = .0; + qreal footerHeight = .0; + if (QQuickItem *header = attached->globalHeader()) { + headerHeight = header->isVisible() ? header->height() : .0; + header->setWidth(width + sepWidth); + header->setPosition(QPointF(pageX, .0)); + header->setZ(2); + if (m_view->separatorVisible()) { + QQuickItem *sep = ensureTrailingSeparator(header); + sep->setProperty("inToolBar", true); + } + } + if (QQuickItem *footer = attached->globalFooter()) { + footerHeight = footer->isVisible() ? footer->height() : .0; + footer->setWidth(width + sepWidth); + footer->setPosition(QPointF(pageX, height() - footerHeight)); + footer->setZ(2); + if (m_view->separatorVisible()) { + QQuickItem *sep = ensureTrailingSeparator(footer); + sep->setProperty("inToolBar", true); + } + } + + child->setSize(QSizeF(width + sepWidth, height() - headerHeight - footerHeight)); + child->setPosition(QPointF(pageX, headerHeight)); + child->setZ(1); + + if (partialWidth <= -x()) { + m_leftPinnedSpace = qMax(m_leftPinnedSpace, width); + } else if (partialWidth > -x() + m_view->width() - child->width() + sepWidth) { + m_rightPinnedSpace = qMax(m_rightPinnedSpace, child->width()); + } + + partialWidth += width; + + } else { + const qreal width = childWidth(child); + qreal headerHeight = .0; + qreal footerHeight = .0; + if (QQuickItem *header = attached->globalHeader(); header && qmlEngine(header)) { + if (m_view->separatorVisible()) { + QQuickItem *sep = ensureLeadingSeparator(header); + sep->setProperty("inToolBar", true); + } + headerHeight = header->isVisible() ? header->height() : .0; + header->setWidth(width); + header->setPosition(QPointF(partialWidth, .0)); + header->setZ(1); + auto it = m_trailingSeparators.find(header); + if (it != m_trailingSeparators.end()) { + it.value()->deleteLater(); + m_trailingSeparators.erase(it); + } + } + if (QQuickItem *footer = attached->globalFooter(); footer && qmlEngine(footer)) { + if (m_view->separatorVisible()) { + QQuickItem *sep = ensureLeadingSeparator(footer); + sep->setProperty("inToolBar", true); + } + footerHeight = footer->isVisible() ? footer->height() : .0; + footer->setWidth(width); + footer->setPosition(QPointF(partialWidth, height() - footerHeight)); + footer->setZ(1); + auto it = m_trailingSeparators.find(footer); + if (it != m_trailingSeparators.end()) { + it.value()->deleteLater(); + m_trailingSeparators.erase(it); + } + } + + child->setSize(QSizeF(width, height() - headerHeight - footerHeight)); + + auto it = m_trailingSeparators.find(child); + if (it != m_trailingSeparators.end()) { + it.value()->deleteLater(); + m_trailingSeparators.erase(it); + } + child->setPosition(QPointF(partialWidth, headerHeight)); + child->setZ(0); + + partialWidth += child->width(); + } + } + + if (reverse) { + attached->setIndex(m_items.count() - (++i)); + } else { + attached->setIndex(i++); + } + + implicitWidth += child->implicitWidth(); + + implicitHeight = qMax(implicitHeight, child->implicitHeight()); + } + + setWidth(partialWidth); + + setImplicitWidth(implicitWidth); + setImplicitHeight(implicitHeight); + + m_view->setImplicitWidth(implicitWidth); + m_view->setImplicitHeight(implicitHeight + m_view->topPadding() + m_view->bottomPadding()); + + const qreal newContentX = m_viewAnchorItem ? -m_viewAnchorItem->x() : 0.0; + if (m_shouldAnimate) { + animateX(newContentX); + } else { + setBoundedX(newContentX); + } + + updateVisibleItems(); +} + +void ContentItem::layoutPinnedItems() +{ + if (m_view->columnResizeMode() == ColumnView::SingleColumn) { + return; + } + + qreal partialWidth = 0; + m_leftPinnedSpace = 0; + m_rightPinnedSpace = 0; + + for (QQuickItem *child : std::as_const(m_items)) { + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(child, true)); + + if (child->isVisible()) { + if (attached->isPinned()) { + QQuickItem *sep = nullptr; + int sepWidth = 0; + if (m_view->separatorVisible()) { + sep = ensureTrailingSeparator(child); + sepWidth = (sep ? sep->width() : 0); + } + + const qreal pageX = qMin(qMax(-x(), partialWidth), -x() + m_view->width() - child->width() + sepWidth); + qreal headerHeight = .0; + qreal footerHeight = .0; + if (QQuickItem *header = attached->globalHeader()) { + headerHeight = header->isVisible() ? header->height() : .0; + header->setPosition(QPointF(pageX, .0)); + if (m_view->separatorVisible()) { + QQuickItem *sep = ensureTrailingSeparator(header); + sep->setProperty("inToolBar", true); + } + } + if (QQuickItem *footer = attached->globalFooter()) { + footerHeight = footer->isVisible() ? footer->height() : .0; + footer->setPosition(QPointF(pageX, height() - footerHeight)); + if (m_view->separatorVisible()) { + QQuickItem *sep = ensureTrailingSeparator(footer); + sep->setProperty("inToolBar", true); + } + } + child->setPosition(QPointF(pageX, headerHeight)); + + if (partialWidth <= -x()) { + m_leftPinnedSpace = qMax(m_leftPinnedSpace, child->width() - sepWidth); + } else if (partialWidth > -x() + m_view->width() - child->width() + sepWidth) { + m_rightPinnedSpace = qMax(m_rightPinnedSpace, child->width()); + } + } + + partialWidth += child->width(); + } + } +} + +void ContentItem::updateVisibleItems() +{ + QList newItems; + + for (auto *item : std::as_const(m_items)) { + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + + if (item->isVisible() && item->x() + x() < m_view->width() && item->x() + item->width() + x() > 0) { + newItems << item; + connect(item, &QObject::destroyed, this, [this, item] { + m_visibleItems.removeAll(item); + }); + attached->setInViewport(true); + item->setEnabled(true); + } else { + attached->setInViewport(false); + item->setEnabled(false); + } + } + + for (auto *item : std::as_const(m_visibleItems)) { + disconnect(item, &QObject::destroyed, this, nullptr); + } + + const QQuickItem *oldLeadingVisibleItem = m_view->leadingVisibleItem(); + const QQuickItem *oldTrailingVisibleItem = m_view->trailingVisibleItem(); + + if (newItems != m_visibleItems) { + m_visibleItems = newItems; + Q_EMIT m_view->visibleItemsChanged(); + if (!m_visibleItems.isEmpty() && m_visibleItems.first() != oldLeadingVisibleItem) { + Q_EMIT m_view->leadingVisibleItemChanged(); + } + if (!m_visibleItems.isEmpty() && m_visibleItems.last() != oldTrailingVisibleItem) { + Q_EMIT m_view->trailingVisibleItemChanged(); + } + } +} + +void ContentItem::forgetItem(QQuickItem *item) +{ + if (!m_items.contains(item)) { + return; + } + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + attached->setView(nullptr); + attached->setIndex(-1); + + disconnect(attached, nullptr, this, nullptr); + disconnect(item, nullptr, this, nullptr); + disconnect(item, nullptr, m_view, nullptr); + + QQuickItem *separatorItem = m_leadingSeparators.take(item); + if (separatorItem) { + separatorItem->deleteLater(); + } + separatorItem = m_trailingSeparators.take(item); + if (separatorItem) { + separatorItem->deleteLater(); + } + + if (QQuickItem *header = attached->globalHeader()) { + header->setVisible(false); + header->setParentItem(item); + separatorItem = m_leadingSeparators.take(header); + if (separatorItem) { + separatorItem->deleteLater(); + } + separatorItem = m_trailingSeparators.take(header); + if (separatorItem) { + separatorItem->deleteLater(); + } + } + if (QQuickItem *footer = attached->globalFooter()) { + footer->setVisible(false); + footer->setParentItem(item); + separatorItem = m_leadingSeparators.take(footer); + if (separatorItem) { + separatorItem->deleteLater(); + } + separatorItem = m_trailingSeparators.take(footer); + if (separatorItem) { + separatorItem->deleteLater(); + } + } + + const int index = m_items.indexOf(item); + m_items.removeAll(item); + // We are connected not only to destroyed but also to lambdas + disconnect(item, nullptr, this, nullptr); + updateVisibleItems(); + m_shouldAnimate = true; + m_view->polish(); + + if (index <= m_view->currentIndex()) { + m_view->setCurrentIndex(m_items.isEmpty() ? 0 : qBound(0, index - 1, m_items.count() - 1)); + } + Q_EMIT m_view->countChanged(); +} + +QQuickItem *ContentItem::ensureLeadingSeparator(QQuickItem *item) +{ + QQuickItem *separatorItem = m_leadingSeparators.value(item); + + if (!separatorItem) { + separatorItem = qobject_cast( + QmlComponentsPoolSingleton::instance(qmlEngine(item))->m_leadingSeparatorComponent->beginCreate(QQmlEngine::contextForObject(item))); + if (separatorItem) { + separatorItem->setParent(this); + separatorItem->setParentItem(item); + separatorItem->setZ(9999); + separatorItem->setProperty("column", QVariant::fromValue(item)); + separatorItem->setProperty("view", QVariant::fromValue(m_view)); + QmlComponentsPoolSingleton::instance(qmlEngine(item))->m_leadingSeparatorComponent->completeCreate(); + m_leadingSeparators[item] = separatorItem; + } + } + + return separatorItem; +} + +QQuickItem *ContentItem::ensureTrailingSeparator(QQuickItem *item) +{ + QQuickItem *separatorItem = m_trailingSeparators.value(item); + + if (!separatorItem) { + separatorItem = qobject_cast( + QmlComponentsPoolSingleton::instance(qmlEngine(item))->m_trailingSeparatorComponent->beginCreate(QQmlEngine::contextForObject(item))); + if (separatorItem) { + separatorItem->setParent(this); + separatorItem->setParentItem(item); + separatorItem->setZ(9999); + separatorItem->setProperty("column", QVariant::fromValue(item)); + QmlComponentsPoolSingleton::instance(qmlEngine(item))->m_trailingSeparatorComponent->completeCreate(); + m_trailingSeparators[item] = separatorItem; + } + } + + return separatorItem; +} + +void ContentItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) +{ + if (m_creationInProgress) { + QQuickItem::itemChange(change, value); + return; + } + switch (change) { + case QQuickItem::ItemChildAddedChange: { + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(value.item, true)); + attached->setView(m_view); + + // connect(attached, &ColumnViewAttached::fillWidthChanged, m_view, &ColumnView::polish); + connect(attached, &ColumnViewAttached::fillWidthChanged, this, [this] { + m_view->polish(); + }); + connect(attached, &ColumnViewAttached::reservedSpaceChanged, m_view, &ColumnView::polish); + + value.item->setVisible(true); + + if (!m_items.contains(value.item)) { + connect(value.item, &QQuickItem::widthChanged, m_view, &ColumnView::polish); + QQuickItem *item = value.item; + m_items << item; + connect(item, &QObject::destroyed, this, [this, item]() { + m_view->removeItem(item); + }); + } + + if (m_view->separatorVisible()) { + ensureLeadingSeparator(value.item); + } + + m_shouldAnimate = true; + m_view->polish(); + Q_EMIT m_view->countChanged(); + break; + } + case QQuickItem::ItemChildRemovedChange: { + forgetItem(value.item); + break; + } + case QQuickItem::ItemVisibleHasChanged: + updateVisibleItems(); + if (value.boolValue) { + m_view->polish(); + } + break; + default: + break; + } + QQuickItem::itemChange(change, value); +} + +void ContentItem::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + updateVisibleItems(); + QQuickItem::geometryChange(newGeometry, oldGeometry); +} + +void ContentItem::syncItemsOrder() +{ + if (m_items == childItems()) { + return; + } + + m_items = childItems(); + // NOTE: polish() here sometimes gets indefinitely delayed and items changing order isn't seen + layoutItems(); +} + +void ContentItem::updateRepeaterModel() +{ + if (!sender()) { + return; + } + + QObject *modelObj = sender()->property("model").value(); + + if (!modelObj) { + m_models.remove(sender()); + return; + } + + if (m_models[sender()]) { + disconnect(m_models[sender()], nullptr, this, nullptr); + } + + m_models[sender()] = modelObj; + + QAbstractItemModel *qaim = qobject_cast(modelObj); + + if (qaim) { + connect(qaim, &QAbstractItemModel::rowsMoved, this, &ContentItem::syncItemsOrder); + + } else { + connect(modelObj, SIGNAL(childrenChanged()), this, SLOT(syncItemsOrder())); + } +} + +void ContentItem::connectHeader(QQuickItem *oldHeader, QQuickItem *newHeader) +{ + if (oldHeader) { + disconnect(oldHeader, nullptr, this, nullptr); + oldHeader->setParentItem(nullptr); + } + if (newHeader) { + connect(newHeader, &QQuickItem::heightChanged, this, &ContentItem::layoutItems); + connect(newHeader, &QQuickItem::visibleChanged, this, &ContentItem::layoutItems); + newHeader->setParentItem(m_globalHeaderParent); + } +} + +void ContentItem::connectFooter(QQuickItem *oldFooter, QQuickItem *newFooter) +{ + if (oldFooter) { + disconnect(oldFooter, nullptr, this, nullptr); + oldFooter->setParentItem(nullptr); + } + if (newFooter) { + connect(newFooter, &QQuickItem::heightChanged, this, &ContentItem::layoutItems); + connect(newFooter, &QQuickItem::visibleChanged, this, &ContentItem::layoutItems); + newFooter->setParentItem(m_globalFooterParent); + } +} + +ColumnView::ColumnView(QQuickItem *parent) + : QQuickItem(parent) + , m_contentItem(nullptr) +{ + // NOTE: this is to *not* trigger itemChange + m_contentItem = new ContentItem(this); + // Prevent interactions outside of ColumnView bounds, and let it act as a viewport. + setClip(true); + setAcceptedMouseButtons(Qt::LeftButton | Qt::BackButton | Qt::ForwardButton); + setAcceptTouchEvents(false); // Relies on synthetized mouse events + setFiltersChildMouseEvents(true); + + connect(m_contentItem->m_slideAnim, &QPropertyAnimation::finished, this, [this]() { + m_moving = false; + Q_EMIT movingChanged(); + }); + connect(m_contentItem, &ContentItem::widthChanged, this, &ColumnView::contentWidthChanged); + connect(m_contentItem, &ContentItem::xChanged, this, &ColumnView::contentXChanged); + + connect(this, &ColumnView::activeFocusChanged, this, [this]() { + if (hasActiveFocus() && m_currentItem) { + m_currentItem->forceActiveFocus(); + } + }); + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(this, true)); + attached->setView(this); + attached = qobject_cast(qmlAttachedPropertiesObject(m_contentItem, true)); + attached->setView(this); +} + +ColumnView::~ColumnView() +{ +} + +ColumnView::ColumnResizeMode ColumnView::columnResizeMode() const +{ + return m_contentItem->m_columnResizeMode; +} + +void ColumnView::setColumnResizeMode(ColumnResizeMode mode) +{ + if (m_contentItem->m_columnResizeMode == mode) { + return; + } + + m_contentItem->m_columnResizeMode = mode; + if (mode == SingleColumn && m_currentItem) { + m_contentItem->m_viewAnchorItem = m_currentItem; + } + m_contentItem->m_shouldAnimate = false; + polish(); + Q_EMIT columnResizeModeChanged(); +} + +qreal ColumnView::columnWidth() const +{ + return m_contentItem->m_columnWidth; +} + +void ColumnView::setColumnWidth(qreal width) +{ + // Always forget the internal binding when the user sets anything, even the same value + disconnect(QmlComponentsPoolSingleton::instance(qmlEngine(this)), &QmlComponentsPool::gridUnitChanged, this, nullptr); + + if (m_contentItem->m_columnWidth == width) { + return; + } + + m_contentItem->m_columnWidth = width; + m_contentItem->m_shouldAnimate = false; + polish(); + Q_EMIT columnWidthChanged(); +} + +int ColumnView::currentIndex() const +{ + return m_currentIndex; +} + +void ColumnView::setCurrentIndex(int index) +{ + if (m_currentIndex == index || index < -1 || index >= m_contentItem->m_items.count()) { + return; + } + + m_currentIndex = index; + + if (index == -1) { + m_currentItem.clear(); + + } else { + m_currentItem = m_contentItem->m_items[index]; + Q_ASSERT(m_currentItem); + m_currentItem->forceActiveFocus(); + + // If the current item is not on view, scroll + QRectF mappedCurrent = m_currentItem->mapRectToItem(this, QRectF(QPointF(0, 0), m_currentItem->size())); + + if (m_contentItem->m_slideAnim->state() == QAbstractAnimation::Running) { + mappedCurrent.moveLeft(mappedCurrent.left() + m_contentItem->x() + m_contentItem->m_slideAnim->endValue().toInt()); + } + + // m_contentItem->m_slideAnim->stop(); + + QRectF contentsRect(m_contentItem->m_leftPinnedSpace, // + 0, + width() - m_contentItem->m_rightPinnedSpace - m_contentItem->m_leftPinnedSpace, + height()); + + if (!m_mouseDown) { + if (!contentsRect.contains(mappedCurrent)) { + m_contentItem->m_viewAnchorItem = m_currentItem; + if (qApp->layoutDirection() == Qt::RightToLeft) { + m_contentItem->animateX(-m_currentItem->x() - m_currentItem->width() + width()); + } else { + m_contentItem->animateX(-m_currentItem->x() + m_contentItem->m_leftPinnedSpace); + } + } else { + m_contentItem->snapToItem(); + } + } + } + + Q_EMIT currentIndexChanged(); + Q_EMIT currentItemChanged(); +} + +QQuickItem *ColumnView::currentItem() +{ + return m_currentItem; +} + +QList ColumnView::visibleItems() const +{ + return m_contentItem->m_visibleItems; +} + +QQuickItem *ColumnView::leadingVisibleItem() const +{ + if (m_contentItem->m_visibleItems.isEmpty()) { + return nullptr; + } + + return m_contentItem->m_visibleItems.first(); +} + +QQuickItem *ColumnView::trailingVisibleItem() const +{ + if (m_contentItem->m_visibleItems.isEmpty()) { + return nullptr; + } + + return qobject_cast(m_contentItem->m_visibleItems.last()); +} + +int ColumnView::count() const +{ + return m_contentItem->m_items.count(); +} + +qreal ColumnView::topPadding() const +{ + return m_topPadding; +} + +void ColumnView::setTopPadding(qreal padding) +{ + if (padding == m_topPadding) { + return; + } + + m_topPadding = padding; + polish(); + Q_EMIT topPaddingChanged(); +} + +qreal ColumnView::bottomPadding() const +{ + return m_bottomPadding; +} + +void ColumnView::setBottomPadding(qreal padding) +{ + if (padding == m_bottomPadding) { + return; + } + + m_bottomPadding = padding; + polish(); + Q_EMIT bottomPaddingChanged(); +} + +QQuickItem *ColumnView::contentItem() const +{ + return m_contentItem; +} + +int ColumnView::scrollDuration() const +{ + return m_contentItem->m_slideAnim->duration(); +} + +void ColumnView::setScrollDuration(int duration) +{ + disconnect(QmlComponentsPoolSingleton::instance(qmlEngine(this)), &QmlComponentsPool::longDurationChanged, this, nullptr); + + if (m_contentItem->m_slideAnim->duration() == duration) { + return; + } + + m_contentItem->m_slideAnim->setDuration(duration); + Q_EMIT scrollDurationChanged(); +} + +bool ColumnView::separatorVisible() const +{ + return m_separatorVisible; +} + +void ColumnView::setSeparatorVisible(bool visible) +{ + if (visible == m_separatorVisible) { + return; + } + + m_separatorVisible = visible; + + Q_EMIT separatorVisibleChanged(); +} + +bool ColumnView::dragging() const +{ + return m_dragging; +} + +bool ColumnView::moving() const +{ + return m_moving; +} + +qreal ColumnView::contentWidth() const +{ + return m_contentItem->width(); +} + +qreal ColumnView::contentX() const +{ + return -m_contentItem->x(); +} + +void ColumnView::setContentX(qreal x) const +{ + m_contentItem->setX(qRound(-x)); +} + +bool ColumnView::interactive() const +{ + return m_interactive; +} + +void ColumnView::setInteractive(bool interactive) +{ + if (m_interactive == interactive) { + return; + } + + m_interactive = interactive; + + if (!m_interactive) { + if (m_dragging) { + m_dragging = false; + Q_EMIT draggingChanged(); + } + + m_contentItem->snapToItem(); + setKeepMouseGrab(false); + } + + Q_EMIT interactiveChanged(); +} + +bool ColumnView::acceptsMouse() const +{ + return m_acceptsMouse; +} + +void ColumnView::setAcceptsMouse(bool accepts) +{ + if (m_acceptsMouse == accepts) { + return; + } + + m_acceptsMouse = accepts; + + if (!m_acceptsMouse) { + if (m_dragging) { + m_dragging = false; + Q_EMIT draggingChanged(); + } + + m_contentItem->snapToItem(); + setKeepMouseGrab(false); + } + + Q_EMIT acceptsMouseChanged(); +} + +void ColumnView::addItem(QQuickItem *item) +{ + insertItem(m_contentItem->m_items.length(), item); +} + +void ColumnView::insertItem(int pos, QQuickItem *item) +{ + if (!item || m_contentItem->m_items.contains(item)) { + return; + } + + m_contentItem->m_items.insert(qBound(0, pos, m_contentItem->m_items.length()), item); + + connect(item, &QObject::destroyed, m_contentItem, [this, item]() { + removeItem(item); + }); + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + attached->setOriginalParent(item->parentItem()); + attached->setShouldDeleteOnRemove(item->parentItem() == nullptr && QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership); + item->setParentItem(m_contentItem); + connect(item, &QQuickItem::implicitWidthChanged, this, &ColumnView::polish); + + item->forceActiveFocus(); + + if (attached->globalHeader()) { + m_contentItem->connectHeader(nullptr, attached->globalHeader()); + } + if (attached->globalFooter()) { + m_contentItem->connectFooter(nullptr, attached->globalFooter()); + } + connect(attached, &ColumnViewAttached::globalHeaderChanged, m_contentItem, &ContentItem::connectHeader); + connect(attached, &ColumnViewAttached::globalFooterChanged, m_contentItem, &ContentItem::connectFooter); + + // Animate shift to new item. + m_contentItem->m_shouldAnimate = true; + m_contentItem->layoutItems(); + Q_EMIT contentChildrenChanged(); + + // In order to keep the same current item we need to increase the current index if displaced + // NOTE: just updating m_currentIndex does *not* update currentItem (which is what we need atm) while setCurrentIndex will update also currentItem + if (m_currentIndex >= pos) { + ++m_currentIndex; + Q_EMIT currentIndexChanged(); + } + + Q_EMIT itemInserted(pos, item); +} + +void ColumnView::replaceItem(int pos, QQuickItem *item) +{ + if (pos < 0 || pos >= m_contentItem->m_items.length()) { + qCWarning(KirigamiLayoutsLog) << "Position" << pos << "passed to ColumnView::replaceItem is out of range."; + return; + } + + if (!item) { + qCWarning(KirigamiLayoutsLog) << "Null item passed to ColumnView::replaceItem."; + return; + } + + QQuickItem *oldItem = m_contentItem->m_items[pos]; + + // In order to keep the same current item we need to increase the current index if displaced + if (m_currentIndex >= pos) { + setCurrentIndex(m_currentIndex - 1); + } + + m_contentItem->forgetItem(oldItem); + oldItem->setVisible(false); + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(oldItem, false)); + + if (attached && attached->shouldDeleteOnRemove()) { + oldItem->deleteLater(); + } else { + oldItem->setParentItem(attached ? attached->originalParent() : nullptr); + } + + Q_EMIT itemRemoved(oldItem); + + if (!m_contentItem->m_items.contains(item)) { + m_contentItem->m_items.insert(qBound(0, pos, m_contentItem->m_items.length()), item); + + connect(item, &QObject::destroyed, m_contentItem, [this, item]() { + removeItem(item); + }); + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + attached->setOriginalParent(item->parentItem()); + attached->setShouldDeleteOnRemove(item->parentItem() == nullptr && QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership); + item->setParentItem(m_contentItem); + connect(item, &QQuickItem::implicitWidthChanged, this, &ColumnView::polish); + + if (attached->globalHeader()) { + m_contentItem->connectHeader(nullptr, attached->globalHeader()); + } + if (attached->globalFooter()) { + m_contentItem->connectFooter(nullptr, attached->globalFooter()); + } + connect(attached, &ColumnViewAttached::globalHeaderChanged, m_contentItem, &ContentItem::connectHeader); + connect(attached, &ColumnViewAttached::globalFooterChanged, m_contentItem, &ContentItem::connectFooter); + + if (m_currentIndex >= pos) { + ++m_currentIndex; + Q_EMIT currentIndexChanged(); + } + + Q_EMIT itemInserted(pos, item); + } + + // Disable animation so replacement happens immediately. + m_contentItem->m_shouldAnimate = false; + m_contentItem->layoutItems(); + Q_EMIT contentChildrenChanged(); +} + +void ColumnView::moveItem(int from, int to) +{ + if (m_contentItem->m_items.isEmpty() // + || from < 0 || from >= m_contentItem->m_items.length() // + || to < 0 || to >= m_contentItem->m_items.length()) { + return; + } + + m_contentItem->m_items.move(from, to); + m_contentItem->m_shouldAnimate = true; + + if (from == m_currentIndex) { + m_currentIndex = to; + Q_EMIT currentIndexChanged(); + } else if (from < m_currentIndex && to > m_currentIndex) { + --m_currentIndex; + Q_EMIT currentIndexChanged(); + } else if (from > m_currentIndex && to <= m_currentIndex) { + ++m_currentIndex; + Q_EMIT currentIndexChanged(); + } + + polish(); +} + +QQuickItem *ColumnView::removeItem(QQuickItem *item) +{ + if (m_contentItem->m_items.isEmpty() || !m_contentItem->m_items.contains(item)) { + return nullptr; + } + + const int index = m_contentItem->m_items.indexOf(item); + + // In order to keep the same current item we need to increase the current index if displaced + if (m_currentIndex >= index) { + setCurrentIndex(m_currentIndex - 1); + } + + m_contentItem->forgetItem(item); + item->setVisible(false); + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, false)); + + if (attached && attached->shouldDeleteOnRemove()) { + item->deleteLater(); + } else { + item->setParentItem(attached ? attached->originalParent() : nullptr); + } + + Q_EMIT contentChildrenChanged(); + Q_EMIT itemRemoved(item); + + return item; +} + +QQuickItem *ColumnView::removeItem(const int index) +{ + if (m_contentItem->m_items.isEmpty() || index < 0 || index >= count()) { + return nullptr; + } else { + return removeItem(m_contentItem->m_items[index]); + } +} + +QQuickItem *ColumnView::removeItem(const QVariant &item) +{ + if (item.canConvert()) { + return removeItem(item.value()); + } else if (item.canConvert()) { + return removeItem(item.toInt()); + } else { + return nullptr; + } +} + +QQuickItem *ColumnView::pop(const QVariant &item) +{ + if (item.canConvert()) { + return pop(item.value()); + } else if (item.canConvert()) { + return pop(item.toInt()); + } else if (item.isNull()) { + return pop(); + } + return nullptr; +} +QQuickItem *ColumnView::pop(QQuickItem *item) +{ + QQuickItem *removed = nullptr; + + while (!m_contentItem->m_items.isEmpty() && m_contentItem->m_items.last() != item) { + removed = removeItem(m_contentItem->m_items.last()); + } + return removed; +} + +QQuickItem *ColumnView::pop(const int index) +{ + if (index >= 0 && index < count() - 1) { + return pop(m_contentItem->m_items.at(index)); + } else if (index == -1) { + return pop(nullptr); + } + return nullptr; +} + +QQuickItem *ColumnView::pop() +{ + if (count() > 0) { + return removeItem(count() - 1); + } + return nullptr; +} + +void ColumnView::clear() +{ + // Don't do an iterator on a list that gets progressively destroyed, treat it as a stack + while (!m_contentItem->m_items.isEmpty()) { + QQuickItem *item = m_contentItem->m_items.first(); + removeItem(item); + } + + m_contentItem->m_items.clear(); + Q_EMIT contentChildrenChanged(); +} + +bool ColumnView::containsItem(QQuickItem *item) +{ + return m_contentItem->m_items.contains(item); +} + +QQuickItem *ColumnView::itemAt(qreal x, qreal y) +{ + return m_contentItem->childAt(x, y); +} + +ColumnViewAttached *ColumnView::qmlAttachedProperties(QObject *object) +{ + return new ColumnViewAttached(object); +} + +void ColumnView::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + m_contentItem->setY(m_topPadding); + m_contentItem->setHeight(newGeometry.height() - m_topPadding - m_bottomPadding); + m_contentItem->m_shouldAnimate = false; + polish(); + + m_contentItem->updateVisibleItems(); + QQuickItem::geometryChange(newGeometry, oldGeometry); +} + +bool ColumnView::childMouseEventFilter(QQuickItem *item, QEvent *event) +{ + if (!m_interactive || item == m_contentItem) { + return QQuickItem::childMouseEventFilter(item, event); + } + + switch (event->type()) { + case QEvent::MouseButtonPress: { + QMouseEvent *me = static_cast(event); + + if (me->button() != Qt::LeftButton) { + return false; + } + + // On press, we set the current index of the view to the root item + QQuickItem *candidateItem = item; + while (candidateItem->parentItem() && candidateItem->parentItem() != m_contentItem) { + candidateItem = candidateItem->parentItem(); + } + if (int idx = m_contentItem->m_items.indexOf(candidateItem); idx >= 0 && candidateItem->parentItem() == m_contentItem) { + setCurrentIndex(idx); + } + + // if !m_acceptsMouse we don't drag with mouse + if (!m_acceptsMouse && me->source() == Qt::MouseEventNotSynthesized) { + event->setAccepted(false); + return false; + } + + m_contentItem->m_slideAnim->stop(); + if (item->property("preventStealing").toBool()) { + m_contentItem->snapToItem(); + return false; + } + m_oldMouseX = m_startMouseX = mapFromItem(item, me->position()).x(); + m_oldMouseY = m_startMouseY = mapFromItem(item, me->position()).y(); + + m_mouseDown = true; + me->setAccepted(false); + setKeepMouseGrab(false); + + break; + } + case QEvent::MouseMove: { + QMouseEvent *me = static_cast(event); + + if (!m_acceptsMouse && me->source() == Qt::MouseEventNotSynthesized) { + return false; + } + + if (!(me->buttons() & Qt::LeftButton)) { + return false; + } + + // It's possible the synthetyzed mouse press event was not passed + // but now we get the mouse move events, consider the first move + // as the press + if (!m_mouseDown) { + m_mouseDown = true; + m_oldMouseX = m_startMouseX = mapFromItem(item, me->position()).x(); + m_oldMouseY = m_startMouseY = mapFromItem(item, me->position()).y(); + return false; + } + + const QPointF pos = mapFromItem(item, me->position()); + + bool verticalScrollIntercepted = false; + + QQuickItem *candidateItem = item; + while (candidateItem->parentItem() && candidateItem->parentItem() != m_contentItem) { + candidateItem = candidateItem->parentItem(); + } + if (candidateItem->parentItem() == m_contentItem) { + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(candidateItem, true)); + if (attached->preventStealing()) { + return false; + } + } + + { + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(candidateItem, true)); + + ScrollIntentionEvent scrollIntentionEvent; + scrollIntentionEvent.delta = QPointF(pos.x() - m_oldMouseX, pos.y() - m_oldMouseY); + + Q_EMIT attached->scrollIntention(&scrollIntentionEvent); + + if (scrollIntentionEvent.accepted) { + verticalScrollIntercepted = true; + event->setAccepted(true); + } + } + + if ((!keepMouseGrab() && (item->keepMouseGrab() || item->keepTouchGrab())) || item->property("preventStealing").toBool()) { + m_contentItem->snapToItem(); + m_oldMouseX = pos.x(); + m_oldMouseY = pos.y(); + return false; + } + + const bool wasDragging = m_dragging; + // If a drag happened, start to steal all events, use startDragDistance * 2 to give time to widgets to take the mouse grab by themselves + m_dragging = keepMouseGrab() || qAbs(mapFromItem(item, me->position()).x() - m_startMouseX) > qApp->styleHints()->startDragDistance() * 3; + + if (m_dragging != wasDragging) { + m_moving = true; + Q_EMIT movingChanged(); + Q_EMIT draggingChanged(); + } + + if (m_dragging) { + m_contentItem->setBoundedX(m_contentItem->x() + pos.x() - m_oldMouseX); + } + + m_contentItem->m_lastDragDelta = pos.x() - m_oldMouseX; + m_oldMouseX = pos.x(); + m_oldMouseY = pos.y(); + + setKeepMouseGrab(m_dragging); + me->setAccepted(m_dragging); + + return m_dragging && !verticalScrollIntercepted; + } + case QEvent::MouseButtonRelease: { + QMouseEvent *me = static_cast(event); + if (item->property("preventStealing").toBool()) { + return false; + } + + if (me->button() == Qt::BackButton && m_currentIndex > 0) { + setCurrentIndex(m_currentIndex - 1); + me->accept(); + return true; + } else if (me->button() == Qt::ForwardButton) { + setCurrentIndex(m_currentIndex + 1); + me->accept(); + return true; + } + + if (!m_acceptsMouse && me->source() == Qt::MouseEventNotSynthesized) { + return false; + } + + if (me->button() != Qt::LeftButton) { + return false; + } + + m_mouseDown = false; + + if (m_dragging) { + m_contentItem->snapToItem(); + m_contentItem->m_lastDragDelta = 0; + m_dragging = false; + Q_EMIT draggingChanged(); + } + + event->accept(); + + // if a drag happened, don't pass the event + const bool block = keepMouseGrab(); + setKeepMouseGrab(false); + + me->setAccepted(block); + return block; + } + default: + break; + } + + return QQuickItem::childMouseEventFilter(item, event); +} + +void ColumnView::mousePressEvent(QMouseEvent *event) +{ + if (!m_acceptsMouse && event->source() == Qt::MouseEventNotSynthesized) { + event->setAccepted(false); + return; + } + + if (event->button() == Qt::BackButton || event->button() == Qt::ForwardButton) { + event->accept(); + return; + } + + if (!m_interactive) { + return; + } + + m_contentItem->snapToItem(); + m_oldMouseX = event->position().x(); + m_startMouseX = event->position().x(); + m_mouseDown = true; + setKeepMouseGrab(false); + event->accept(); +} + +void ColumnView::mouseMoveEvent(QMouseEvent *event) +{ + if (event->buttons() & Qt::BackButton || event->buttons() & Qt::ForwardButton) { + event->accept(); + return; + } + + if (!m_interactive) { + return; + } + + const bool wasDragging = m_dragging; + // Same startDragDistance * 2 as the event filter + m_dragging = keepMouseGrab() || qAbs(event->position().x() - m_startMouseX) > qApp->styleHints()->startDragDistance() * 2; + if (m_dragging != wasDragging) { + m_moving = true; + Q_EMIT movingChanged(); + Q_EMIT draggingChanged(); + } + + setKeepMouseGrab(m_dragging); + + if (m_dragging) { + m_contentItem->setBoundedX(m_contentItem->x() + event->pos().x() - m_oldMouseX); + } + + m_contentItem->m_lastDragDelta = event->pos().x() - m_oldMouseX; + m_oldMouseX = event->pos().x(); + event->accept(); +} + +void ColumnView::mouseReleaseEvent(QMouseEvent *event) +{ + if (event->button() == Qt::BackButton && m_currentIndex > 0) { + setCurrentIndex(m_currentIndex - 1); + event->accept(); + return; + } else if (event->button() == Qt::ForwardButton) { + setCurrentIndex(m_currentIndex + 1); + event->accept(); + return; + } + + m_mouseDown = false; + + if (!m_interactive) { + return; + } + + m_contentItem->snapToItem(); + m_contentItem->m_lastDragDelta = 0; + + if (m_dragging) { + m_dragging = false; + Q_EMIT draggingChanged(); + } + + setKeepMouseGrab(false); + event->accept(); +} + +void ColumnView::mouseUngrabEvent() +{ + m_mouseDown = false; + + if (m_contentItem->m_slideAnim->state() != QAbstractAnimation::Running) { + m_contentItem->snapToItem(); + } + m_contentItem->m_lastDragDelta = 0; + + if (m_dragging) { + m_dragging = false; + Q_EMIT draggingChanged(); + } + + setKeepMouseGrab(false); +} + +void ColumnView::classBegin() +{ + auto syncColumnWidth = [this]() { + m_contentItem->m_columnWidth = privateQmlComponentsPoolSelf->instance(qmlEngine(this))->m_units->gridUnit() * 20; + Q_EMIT columnWidthChanged(); + }; + + connect(QmlComponentsPoolSingleton::instance(qmlEngine(this)), &QmlComponentsPool::gridUnitChanged, this, syncColumnWidth); + syncColumnWidth(); + + auto syncDuration = [this]() { + m_contentItem->m_slideAnim->setDuration(QmlComponentsPoolSingleton::instance(qmlEngine(this))->m_units->veryLongDuration()); + Q_EMIT scrollDurationChanged(); + }; + + connect(QmlComponentsPoolSingleton::instance(qmlEngine(this)), &QmlComponentsPool::longDurationChanged, this, syncDuration); + syncDuration(); + + QQuickItem::classBegin(); +} + +void ColumnView::componentComplete() +{ + m_complete = true; + QQuickItem::componentComplete(); +} + +void ColumnView::updatePolish() +{ + m_contentItem->layoutItems(); +} + +void ColumnView::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) +{ + switch (change) { + case QQuickItem::ItemChildAddedChange: + if (m_contentItem && value.item != m_contentItem && !value.item->inherits("QQuickRepeater")) { + addItem(value.item); + } + break; + default: + break; + } + QQuickItem::itemChange(change, value); +} + +void ColumnView::contentChildren_append(QQmlListProperty *prop, QQuickItem *item) +{ + // This can only be called from QML + ColumnView *view = static_cast(prop->object); + if (!view) { + return; + } + + view->m_contentItem->m_items.append(item); + connect(item, &QObject::destroyed, view->m_contentItem, [view, item]() { + view->removeItem(item); + }); + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + attached->setOriginalParent(item->parentItem()); + attached->setShouldDeleteOnRemove(item->parentItem() == nullptr && QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership); + + item->setParentItem(view->m_contentItem); +} + +qsizetype ColumnView::contentChildren_count(QQmlListProperty *prop) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return 0; + } + + return view->m_contentItem->m_items.count(); +} + +QQuickItem *ColumnView::contentChildren_at(QQmlListProperty *prop, qsizetype index) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return nullptr; + } + + if (index < 0 || index >= view->m_contentItem->m_items.count()) { + return nullptr; + } + return view->m_contentItem->m_items.value(index); +} + +void ColumnView::contentChildren_clear(QQmlListProperty *prop) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return; + } + + return view->m_contentItem->m_items.clear(); +} + +QQmlListProperty ColumnView::contentChildren() +{ + return QQmlListProperty(this, // + nullptr, + contentChildren_append, + contentChildren_count, + contentChildren_at, + contentChildren_clear); +} + +void ColumnView::contentData_append(QQmlListProperty *prop, QObject *object) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return; + } + + view->m_contentData.append(object); + QQuickItem *item = qobject_cast(object); + // exclude repeaters from layout + if (item && item->inherits("QQuickRepeater")) { + item->setParentItem(view); + + connect(item, SIGNAL(modelChanged()), view->m_contentItem, SLOT(updateRepeaterModel())); + + } else if (item) { + view->m_contentItem->m_items.append(item); + connect(item, &QObject::destroyed, view->m_contentItem, [view, item]() { + view->removeItem(item); + }); + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + attached->setOriginalParent(item->parentItem()); + attached->setShouldDeleteOnRemove(view->m_complete && !item->parentItem() && QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership); + + item->setParentItem(view->m_contentItem); + + } else { + object->setParent(view); + } +} + +qsizetype ColumnView::contentData_count(QQmlListProperty *prop) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return 0; + } + + return view->m_contentData.count(); +} + +QObject *ColumnView::contentData_at(QQmlListProperty *prop, qsizetype index) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return nullptr; + } + + if (index < 0 || index >= view->m_contentData.count()) { + return nullptr; + } + return view->m_contentData.value(index); +} + +void ColumnView::contentData_clear(QQmlListProperty *prop) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return; + } + + return view->m_contentData.clear(); +} + +QQmlListProperty ColumnView::contentData() +{ + return QQmlListProperty(this, // + nullptr, + contentData_append, + contentData_count, + contentData_at, + contentData_clear); +} + +#include "moc_columnview.cpp" +#include "moc_columnview_p.cpp" diff --git a/src/layouts/columnview.h b/src/layouts/columnview.h new file mode 100644 index 0000000..0ced4d5 --- /dev/null +++ b/src/layouts/columnview.h @@ -0,0 +1,556 @@ +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include + +class ContentItem; +class ColumnView; + +class ScrollIntentionEvent : public QObject +{ + Q_OBJECT + Q_PROPERTY(QPointF delta MEMBER delta CONSTANT FINAL) + Q_PROPERTY(bool accepted MEMBER accepted FINAL) +public: + ScrollIntentionEvent() + { + } + ~ScrollIntentionEvent() override + { + } + + QPointF delta; + bool accepted = false; +}; + +/** + * This is an attached property to every item that is inserted in the ColumnView, + * used to access the view and page information such as the position and information for layouting, such as fillWidth + * @since 2.7 + */ +class ColumnViewAttached : public QObject +{ + Q_OBJECT + + /** + * The index position of the column in the view, starting from 0 + */ + Q_PROPERTY(int index READ index WRITE setIndex NOTIFY indexChanged FINAL) + + /** + * If true, the column will expand to take the whole viewport space minus reservedSpace + */ + Q_PROPERTY(bool fillWidth READ fillWidth WRITE setFillWidth NOTIFY fillWidthChanged FINAL) + + /** + * When a column is fillWidth, it will keep reservedSpace amount of pixels from going to fill the full viewport width + */ + Q_PROPERTY(qreal reservedSpace READ reservedSpace WRITE setReservedSpace NOTIFY reservedSpaceChanged FINAL) + + /** + * Like the same property of MouseArea, when this is true, the column view won't + * try to manage events by itself when filtering from a child, not + * disturbing user interaction + */ + Q_PROPERTY(bool preventStealing READ preventStealing WRITE setPreventStealing NOTIFY preventStealingChanged FINAL) + + /** + * If true the page will never go out of view, but will stay either + * at the right or left side of the ColumnView + */ + Q_PROPERTY(bool pinned READ isPinned WRITE setPinned NOTIFY pinnedChanged FINAL) + + /** + * The view this column belongs to + */ + Q_PROPERTY(ColumnView *view READ view NOTIFY viewChanged FINAL) + + /** + * True if this column is at least partly visible in the ColumnView's viewport. + * @since 5.77 + */ + Q_PROPERTY(bool inViewport READ inViewport NOTIFY inViewportChanged FINAL) + + Q_PROPERTY(QQuickItem *globalHeader READ globalHeader WRITE setGlobalHeader NOTIFY globalHeaderChanged FINAL) + Q_PROPERTY(QQuickItem *globalFooter READ globalFooter WRITE setGlobalFooter NOTIFY globalFooterChanged FINAL) + +public: + ColumnViewAttached(QObject *parent = nullptr); + ~ColumnViewAttached() override; + + void setIndex(int index); + int index() const; + + void setFillWidth(bool fill); + bool fillWidth() const; + + qreal reservedSpace() const; + void setReservedSpace(qreal space); + + ColumnView *view(); + void setView(ColumnView *view); + + // Private API, not for QML use + QQuickItem *originalParent() const; + void setOriginalParent(QQuickItem *parent); + + bool shouldDeleteOnRemove() const; + void setShouldDeleteOnRemove(bool del); + + bool preventStealing() const; + void setPreventStealing(bool prevent); + + bool isPinned() const; + void setPinned(bool pinned); + + bool inViewport() const; + void setInViewport(bool inViewport); + + QQuickItem *globalHeader() const; + void setGlobalHeader(QQuickItem *header); + + QQuickItem *globalFooter() const; + void setGlobalFooter(QQuickItem *footer); + +Q_SIGNALS: + void indexChanged(); + void fillWidthChanged(); + void reservedSpaceChanged(); + void viewChanged(); + void preventStealingChanged(); + void pinnedChanged(); + void scrollIntention(ScrollIntentionEvent *event); + void inViewportChanged(); + void globalHeaderChanged(QQuickItem *oldHeader, QQuickItem *newHeader); + void globalFooterChanged(QQuickItem *oldFooter, QQuickItem *newFooter); + +private: + int m_index = -1; + bool m_fillWidth = false; + qreal m_reservedSpace = 0; + QPointer m_view; + QPointer m_originalParent; + bool m_customFillWidth = false; + bool m_customReservedSpace = false; + bool m_shouldDeleteOnRemove = true; + bool m_preventStealing = false; + bool m_pinned = false; + bool m_inViewport = false; + QPointer m_globalHeader; + QPointer m_globalFooter; +}; + +/** + * ColumnView is a container that lays out items horizontally in a row, + * when not all items fit in the ColumnView, it will behave like a Flickable and will be a scrollable view which shows only a determined number of columns. + * The columns can either all have the same fixed size (recommended), + * size themselves with implicitWidth, or automatically expand to take all the available width: by default the last column will always be the expanding one. + * Items inside the ColumnView can access info of the view and set layouting hints via the ColumnView attached property. + * + * This is the base for the implementation of PageRow + * @since 2.7 + */ +class ColumnView : public QQuickItem +{ + Q_OBJECT + QML_ELEMENT + QML_ATTACHED(ColumnViewAttached) + + /** + * The strategy to follow while automatically resizing the columns, + * the enum can have the following values: + * * FixedColumns: every column is fixed at the same width of the columnWidth property + * * DynamicColumns: columns take their width from their implicitWidth + * * SingleColumn: only one column at a time is shown, as wide as the viewport, eventual reservedSpace on the column's attached property is ignored + * The default is FixedColumns. + */ + Q_PROPERTY(ColumnResizeMode columnResizeMode READ columnResizeMode WRITE setColumnResizeMode NOTIFY columnResizeModeChanged FINAL) + + /** + * The width of all columns when columnResizeMode is FixedColumns + */ + Q_PROPERTY(qreal columnWidth READ columnWidth WRITE setColumnWidth NOTIFY columnWidthChanged FINAL) + + /** + * How many columns this view containsItem*/ + Q_PROPERTY(int count READ count NOTIFY countChanged FINAL) + + /** + * The position of the currently active column. The current column will also have keyboard focus + */ + Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged FINAL) + + /** + * The currently active column. The current column will also have keyboard focus + */ + Q_PROPERTY(QQuickItem *currentItem READ currentItem NOTIFY currentItemChanged FINAL) + + /** + * The main content item of this view: it's the parent of the column items + */ + Q_PROPERTY(QQuickItem *contentItem READ contentItem CONSTANT FINAL) + + /** + * The value of the horizontal scroll of the view, in pixels + */ + Q_PROPERTY(qreal contentX READ contentX WRITE setContentX NOTIFY contentXChanged FINAL) + + /** + * The compound width of all columns in the view + */ + Q_PROPERTY(qreal contentWidth READ contentWidth NOTIFY contentWidthChanged FINAL) + + /** + * The padding this will have at the top + */ + Q_PROPERTY(qreal topPadding READ topPadding WRITE setTopPadding NOTIFY topPaddingChanged FINAL) + + /** + * The padding this will have at the bottom + */ + Q_PROPERTY(qreal bottomPadding READ bottomPadding WRITE setBottomPadding NOTIFY bottomPaddingChanged FINAL) + + /** + * The duration for scrolling animations + */ + Q_PROPERTY(int scrollDuration READ scrollDuration WRITE setScrollDuration NOTIFY scrollDurationChanged FINAL) + + /** + * True if columns should be visually separated by a separator line + */ + Q_PROPERTY(bool separatorVisible READ separatorVisible WRITE setSeparatorVisible NOTIFY separatorVisibleChanged FINAL) + + /** + * The list of all visible column items that are at least partially in the viewport at any given moment + */ + Q_PROPERTY(QList visibleItems READ visibleItems NOTIFY visibleItemsChanged FINAL) + + /** + * The first of visibleItems provided from convenience + */ + Q_PROPERTY(QQuickItem *leadingVisibleItem READ leadingVisibleItem NOTIFY leadingVisibleItemChanged FINAL) + + /** + * The last of visibleItems provided from convenience + */ + Q_PROPERTY(QQuickItem *trailingVisibleItem READ trailingVisibleItem NOTIFY trailingVisibleItemChanged FINAL) + + // Properties to make it similar to Flickable + /** + * True when the user is dragging around with touch gestures the view contents + */ + Q_PROPERTY(bool dragging READ dragging NOTIFY draggingChanged FINAL) + + /** + * True both when the user is dragging around with touch gestures the view contents or the view is animating + */ + Q_PROPERTY(bool moving READ moving NOTIFY movingChanged FINAL) + + /** + * True if it supports moving the contents by dragging + */ + Q_PROPERTY(bool interactive READ interactive WRITE setInteractive NOTIFY interactiveChanged FINAL) + + /** + * True if the contents can be dragged also with mouse besides touch + */ + Q_PROPERTY(bool acceptsMouse READ acceptsMouse WRITE setAcceptsMouse NOTIFY acceptsMouseChanged FINAL) + + // Default properties + /** + * Every column item the view contains + */ + Q_PROPERTY(QQmlListProperty contentChildren READ contentChildren NOTIFY contentChildrenChanged FINAL) + /** + * every item declared inside the view, both visual and non-visual items + */ + Q_PROPERTY(QQmlListProperty contentData READ contentData FINAL) + Q_CLASSINFO("DefaultProperty", "contentData") + +public: + enum ColumnResizeMode { + FixedColumns = 0, + DynamicColumns, + SingleColumn, + }; + Q_ENUM(ColumnResizeMode) + + ColumnView(QQuickItem *parent = nullptr); + ~ColumnView() override; + + // QML property accessors + ColumnResizeMode columnResizeMode() const; + void setColumnResizeMode(ColumnResizeMode mode); + + qreal columnWidth() const; + void setColumnWidth(qreal width); + + int currentIndex() const; + void setCurrentIndex(int index); + + int scrollDuration() const; + void setScrollDuration(int duration); + + bool separatorVisible() const; + void setSeparatorVisible(bool visible); + + int count() const; + + qreal topPadding() const; + void setTopPadding(qreal padding); + + qreal bottomPadding() const; + void setBottomPadding(qreal padding); + + QQuickItem *currentItem(); + + QList visibleItems() const; + QQuickItem *leadingVisibleItem() const; + QQuickItem *trailingVisibleItem() const; + + QQuickItem *contentItem() const; + + QQmlListProperty contentChildren(); + QQmlListProperty contentData(); + + bool dragging() const; + bool moving() const; + qreal contentWidth() const; + + qreal contentX() const; + void setContentX(qreal x) const; + + bool interactive() const; + void setInteractive(bool interactive); + + bool acceptsMouse() const; + void setAcceptsMouse(bool accepts); + + /** + * @brief This method removes all the items after the specified item or + * index from the view and returns the last item that was removed. + * + * Note that if the passed value is neither of the values said below, it + * will return a nullptr. + * + * @param item the item to remove. It can be an item, index or not defined + * in which case it will pop the last item. + */ + Q_INVOKABLE QQuickItem *pop(const QVariant &item); + + /** + * @brief This method removes all the items after the specified item from + * the view and returns the last item that was removed. + * + * @see ::removeItem() + * + * @param the item where the iteration should stop at + * @returns the last item that has been removed + */ + QQuickItem *pop(QQuickItem *item); + + /** + * @brief This method removes all the items after the specified position + * from the view and returns the last item that was removed. + * + * It starts iterating from the last item to the first item calling + * removeItem() for each of them until it reaches the specified position. + * + * @see ::removeItem() + * + * @param the position where the iteration should stop at + * @returns the last item that has been removed + */ + QQuickItem *pop(int index); + + /** + * @brief This method removes the last item from the view and returns it. + * + * This method calls removeItem() on the last item. + * + * @see ::removeItem() + * + * @return the removed item + */ + Q_INVOKABLE QQuickItem *pop(); + + /** + * @brief This method removes the specified item from the view. + * + * Items will be reparented to their old parent. If they have JavaScript + * ownership and they didn't have an old parent, they will be destroyed. + * CurrentIndex may be changed in order to keep the same currentItem + * + * @param item pointer to the item to remove + * @returns the removed item + */ + QQuickItem *removeItem(QQuickItem *item); + + /** + * @brief This method removes an item at a given index from the view. + * + * This method calls removeItem(QQuickItem *item) to remove the item at + * the specified index. + * + * @see ::removeItem(QQuickItem *item) + * + * @param index the index of the item which should be removed + * @return the removed item + */ + QQuickItem *removeItem(int index); + + // QML attached property + static ColumnViewAttached *qmlAttachedProperties(QObject *object); + +public Q_SLOTS: + /** + * Pushes a new item at the end of the view + * @param item the new item which will be reparented and managed + */ + void addItem(QQuickItem *item); + + /** + * @brief This method removes an item from the view. + * + * If the argument is a number, this method dispatches to removeItem(int index) + * to remove an item by its index. Otherwise the argument should be the item + * itself to be removed itself, and this method will dispatch to removeItem(QQuickItem *item). + * + * @see ::removeItem(QQuickItem *item) + * + * @param index the index of the item which should be removed, or the item itself + * @return the removed item + */ + Q_INVOKABLE QQuickItem *removeItem(const QVariant &item); + + /** + * Inserts a new item in the view at a given position. + * The current Item will not be changed, currentIndex will be adjusted + * accordingly if needed to keep the same current item. + * @param pos the position we want the new item to be inserted in + * @param item the new item which will be reparented and managed + */ + void insertItem(int pos, QQuickItem *item); + + /** + * Replaces an item in the view at a given position with a new item. + * The current Item and currentIndex will not be changed. + * @param pos the position we want the new item to be placed in + * @param item the new item which will be reparented and managed + */ + void replaceItem(int pos, QQuickItem *item); + + /** + * Move an item inside the view. + * The currentIndex property may be changed in order to keep currentItem the same. + * @param from the old position + * @param to the new position + */ + void moveItem(int from, int to); + + /** + * Removes every item in the view. + * Items will be reparented to their old parent. + * If they have JavaScript ownership and they didn't have an old parent, they will be destroyed + */ + void clear(); + + /** + * @returns true if the view contains the given item + */ + bool containsItem(QQuickItem *item); + + /** + * Returns the visible item containing the point x, y in content coordinates. + * If there is no item at the point specified, or the item is not visible null is returned. + */ + QQuickItem *itemAt(qreal x, qreal y); + +protected: + void classBegin() override; + void componentComplete() override; + void updatePolish() override; + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) override; + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; + bool childMouseEventFilter(QQuickItem *item, QEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseUngrabEvent() override; + +Q_SIGNALS: + /** + * A new item has been inserted + * @param position where the page has been inserted + * @param item a pointer to the new item + */ + void itemInserted(int position, QQuickItem *item); + + /** + * An item has just been removed from the view + * @param item a pointer to the item that has just been removed + */ + void itemRemoved(QQuickItem *item); + + // Property notifiers + void contentChildrenChanged(); + void columnResizeModeChanged(); + void columnWidthChanged(); + void currentIndexChanged(); + void currentItemChanged(); + void visibleItemsChanged(); + void countChanged(); + void draggingChanged(); + void movingChanged(); + void contentXChanged(); + void contentWidthChanged(); + void interactiveChanged(); + void acceptsMouseChanged(); + void scrollDurationChanged(); + void separatorVisibleChanged(); + void leadingVisibleItemChanged(); + void trailingVisibleItemChanged(); + void topPaddingChanged(); + void bottomPaddingChanged(); + +private: + static void contentChildren_append(QQmlListProperty *prop, QQuickItem *object); + static qsizetype contentChildren_count(QQmlListProperty *prop); + static QQuickItem *contentChildren_at(QQmlListProperty *prop, qsizetype index); + static void contentChildren_clear(QQmlListProperty *prop); + + static void contentData_append(QQmlListProperty *prop, QObject *object); + static qsizetype contentData_count(QQmlListProperty *prop); + static QObject *contentData_at(QQmlListProperty *prop, qsizetype index); + static void contentData_clear(QQmlListProperty *prop); + + QList m_contentData; + + ContentItem *m_contentItem; + QPointer m_currentItem; + + qreal m_oldMouseX = -1.0; + qreal m_startMouseX = -1.0; + qreal m_oldMouseY = -1.0; + qreal m_startMouseY = -1.0; + int m_currentIndex = -1; + qreal m_topPadding = 0; + qreal m_bottomPadding = 0; + + bool m_mouseDown = false; + bool m_interactive = true; + bool m_dragging = false; + bool m_moving = false; + bool m_separatorVisible = true; + bool m_complete = false; + bool m_acceptsMouse = false; +}; + +QML_DECLARE_TYPEINFO(ColumnView, QML_HAS_ATTACHED_PROPERTIES) diff --git a/src/layouts/columnview_p.h b/src/layouts/columnview_p.h new file mode 100644 index 0000000..618f8f1 --- /dev/null +++ b/src/layouts/columnview_p.h @@ -0,0 +1,99 @@ +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include "columnview.h" + +#include +#include + +class QPropertyAnimation; +class QQmlComponent; +namespace Kirigami +{ +namespace Platform +{ +class Units; +} +} + +class QmlComponentsPool : public QObject +{ + Q_OBJECT + +public: + QmlComponentsPool(QQmlEngine *engine); + ~QmlComponentsPool() override; + + QQmlComponent *m_leadingSeparatorComponent = nullptr; + QQmlComponent *m_trailingSeparatorComponent = nullptr; + Kirigami::Platform::Units *m_units = nullptr; + +Q_SIGNALS: + void gridUnitChanged(); + void longDurationChanged(); + +private: + QObject *m_instance = nullptr; +}; + +class ContentItem : public QQuickItem +{ + Q_OBJECT + +public: + ContentItem(ColumnView *parent = nullptr); + ~ContentItem() override; + + void layoutItems(); + void layoutPinnedItems(); + qreal childWidth(QQuickItem *child); + void updateVisibleItems(); + void forgetItem(QQuickItem *item); + QQuickItem *ensureLeadingSeparator(QQuickItem *item); + QQuickItem *ensureTrailingSeparator(QQuickItem *item); + + void setBoundedX(qreal x); + void animateX(qreal x); + void snapToItem(); + + void connectHeader(QQuickItem *oldHeader, QQuickItem *newHeader); + void connectFooter(QQuickItem *oldFooter, QQuickItem *newFooter); + + inline qreal viewportLeft() const; + inline qreal viewportRight() const; + +protected: + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) override; + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; + +private Q_SLOTS: + void syncItemsOrder(); + void updateRepeaterModel(); + +private: + ColumnView *m_view; + QQuickItem *m_globalHeaderParent; + QQuickItem *m_globalFooterParent; + QPropertyAnimation *m_slideAnim; + QList m_items; + QList m_visibleItems; + QPointer m_viewAnchorItem; + QHash m_leadingSeparators; + QHash m_trailingSeparators; + QHash m_models; + + qreal m_leftPinnedSpace = 361; + qreal m_rightPinnedSpace = 0; + + qreal m_columnWidth = 0; + qreal m_lastDragDelta = 0; + ColumnView::ColumnResizeMode m_columnResizeMode = ColumnView::FixedColumns; + bool m_shouldAnimate = false; + bool m_creationInProgress = true; + friend class ColumnView; +}; diff --git a/src/layouts/displayhint.cpp b/src/layouts/displayhint.cpp new file mode 100644 index 0000000..4cf7ea4 --- /dev/null +++ b/src/layouts/displayhint.cpp @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "displayhint.h" + +#include "moc_displayhint.cpp" + +#include + +bool DisplayHint::displayHintSet(DisplayHints values, Hint hint) +{ + return isDisplayHintSet(values, hint); +} + +bool DisplayHint::displayHintSet(QObject *object, DisplayHint::Hint hint) +{ + if (!object) { + return false; + } + + auto property = object->property("displayHint"); + if (property.isValid()) { + return isDisplayHintSet(DisplayHints{property.toInt()}, hint); + } else { + return false; + } +} + +bool DisplayHint::isDisplayHintSet(DisplayHint::DisplayHints values, DisplayHint::Hint hint) +{ + if (hint == DisplayHint::AlwaysHide && (values & DisplayHint::KeepVisible)) { + return false; + } + + return values & hint; +} diff --git a/src/layouts/displayhint.h b/src/layouts/displayhint.h new file mode 100644 index 0000000..517924d --- /dev/null +++ b/src/layouts/displayhint.h @@ -0,0 +1,96 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef DISPLAYHINT_H +#define DISPLAYHINT_H + +#include +#include + +class DisplayHint : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + /** + * Hints for implementations using Actions indicating preferences about how to display the action. + */ + enum Hint : uint { + /** + * Indicates there is no specific preference. + */ + NoPreference = 0, + /** + * Only display an icon for this Action. + */ + IconOnly = 1, + /** + * Try to keep the action visible even when space constrained. + * Mutually exclusive with AlwaysHide, KeepVisible has priority. + */ + KeepVisible = 2, + /** + * If possible, hide the action in an overflow menu or similar location. + * Mutually exclusive with KeepVisible, KeepVisible has priority. + */ + AlwaysHide = 4, + /** + * When this action has children, do not display any indicator (like a + * menu arrow) for this action. + */ + HideChildIndicator = 8, + }; + Q_DECLARE_FLAGS(DisplayHints, Hint) + Q_ENUM(Hint) + Q_FLAG(DisplayHints) + + // Note: These functions are instance methods because they need to be + // exposed to QML. Unfortunately static methods are not supported. + + /** + * Helper function to check if a certain display hint has been set. + * + * This function is mostly convenience to enforce certain behaviour of the + * various display hints, primarily the mutual exclusivity of KeepVisible + * and AlwaysHide. + * + * @param values The display hints to check. + * @param hint The display hint to check if it is set. + * + * @return true if the hint was set for this action, false if not. + * + * @since 2.14 + */ + Q_INVOKABLE bool displayHintSet(DisplayHints values, Hint hint); + + /** + * Check if a certain display hint has been set on an object. + * + * This overloads @f displayHintSet(DisplayHints, Hint) to accept a QObject + * instance. This object is checked to see if it has a displayHint property + * and if so, if that property has @p hint set. + * + * @param object The object to check. + * @param hint The hint to check for. + * + * @return false if object is null, object has no displayHint property or + * the hint was not set. true if it has the property and the hint is + * set. + */ + Q_INVOKABLE bool displayHintSet(QObject *object, Hint hint); + + /** + * Static version of \f displayHintSet(DisplayHints, Hint) that can be + * called from C++ code. + */ + static bool isDisplayHintSet(DisplayHints values, Hint hint); +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(DisplayHint::DisplayHints) + +#endif // DISPLAYHINT_H diff --git a/src/layouts/formlayoutattached.cpp b/src/layouts/formlayoutattached.cpp new file mode 100644 index 0000000..1e52c11 --- /dev/null +++ b/src/layouts/formlayoutattached.cpp @@ -0,0 +1,121 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "formlayoutattached.h" +#include "loggingcategory.h" + +#include +#include + +FormLayoutAttached::FormLayoutAttached(QObject *parent) + : QObject(parent) +{ + m_buddyFor = qobject_cast(parent); + if (!m_buddyFor) { + qWarning(KirigamiLayoutsLog) << "FormData must be attached to an Item"; + } +} + +FormLayoutAttached::~FormLayoutAttached() +{ +} + +void FormLayoutAttached::setLabel(const QString &text) +{ + if (m_label == text) { + return; + } + + m_label = text; + Q_EMIT labelChanged(); +} + +QString FormLayoutAttached::label() const +{ + return m_label; +} + +void FormLayoutAttached::setLabelAlignment(int alignment) +{ + if (m_labelAlignment == alignment) { + return; + } + + m_labelAlignment = alignment; + Q_EMIT labelAlignmentChanged(); +} + +int FormLayoutAttached::labelAlignment() const +{ + return m_labelAlignment; +} + +void FormLayoutAttached::setIsSection(bool section) +{ + if (m_isSection == section) { + return; + } + + m_isSection = section; + Q_EMIT isSectionChanged(); +} + +bool FormLayoutAttached::isSection() const +{ + return m_isSection; +} + +QQuickItem *FormLayoutAttached::buddyFor() const +{ + return m_buddyFor; +} + +void FormLayoutAttached::setBuddyFor(QQuickItem *aBuddyFor) +{ + if (m_buddyFor == aBuddyFor) { + return; + } + + const auto attachee = qobject_cast(parent()); + + if (!attachee) { + return; + } + + // TODO: Use ScenePosition or introduce new type for optimized relative + // position calculation to support more nested buddy. + + if (aBuddyFor && aBuddyFor != attachee && aBuddyFor->parentItem() != attachee) { + qWarning(KirigamiLayoutsLog).nospace() << "FormData.buddyFor must be a direct child of the attachee. Attachee: " << attachee + << ", buddyFor: " << aBuddyFor; + return; + } + + if (m_buddyFor) { + disconnect(m_buddyFor, &QObject::destroyed, this, &FormLayoutAttached::resetBuddyFor); + } + + m_buddyFor = aBuddyFor; + + if (m_buddyFor) { + connect(m_buddyFor, &QObject::destroyed, this, &FormLayoutAttached::resetBuddyFor); + } + + Q_EMIT buddyForChanged(); +} + +void FormLayoutAttached::resetBuddyFor() +{ + const auto attachee = qobject_cast(parent()); + setBuddyFor(attachee); +} + +FormLayoutAttached *FormLayoutAttached::qmlAttachedProperties(QObject *object) +{ + return new FormLayoutAttached(object); +} + +#include "moc_formlayoutattached.cpp" diff --git a/src/layouts/formlayoutattached.h b/src/layouts/formlayoutattached.h new file mode 100644 index 0000000..e43c046 --- /dev/null +++ b/src/layouts/formlayoutattached.h @@ -0,0 +1,171 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef FORMLAYOUTATTACHED_H +#define FORMLAYOUTATTACHED_H + +#include +#include + +class QQuickItem; + +/** + * This attached property contains the information for decorating a org::kde::kirigami::FormLayout: + * + * It contains the text labels of fields and information about sections. + * + * Some of its properties can be used with other Layout types. + * @code{.qml} + * import org.kde.kirigami as Kirigami + * + * Kirigami.FormLayout { + * TextField { + * Kirigami.FormData.label: "User:" + * } + * TextField { + * Kirigami.FormData.label: "Password:" + * } + * } + * @endcode + * @see org::kde::kirigami::FormLayout + * @since 2.3 + */ +class FormLayoutAttached : public QObject +{ + Q_OBJECT + QML_NAMED_ELEMENT(FormData) + QML_ATTACHED(FormLayoutAttached) + QML_UNCREATABLE("") + /** + * The label for a org::kde::kirigami::FormLayout field + */ + Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged FINAL) + /** + * The alignment for the label of a org::kde::kirigami::FormLayout field + */ + Q_PROPERTY(int labelAlignment READ labelAlignment WRITE setLabelAlignment NOTIFY labelAlignmentChanged FINAL) + /** + * If true, the child item of a org::kde::kirigami::FormLayout becomes a section separator, and + * may have different looks: + * * To make it just a space between two fields, just put an empty item with FormData.isSection: + * @code + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * Item { + * Kirigami.FormData.isSection: true + * } + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * @endcode + * + * * To make it a space with a section title: + * @code + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * Item { + * Kirigami.FormData.label: "Section Title" + * Kirigami.FormData.isSection: true + * } + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * @endcode + * + * * To make it a space with a section title and a separator line: + * @code + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * Kirigami.Separator { + * Kirigami.FormData.label: "Section Title" + * Kirigami.FormData.isSection: true + * } + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * @endcode + * @see org::kde::kirigami::FormLayout + */ + Q_PROPERTY(bool isSection READ isSection WRITE setIsSection NOTIFY isSectionChanged FINAL) + + /** + * This property can only be used + * in conjunction with a Kirigami.FormData.label, + * often in a layout that is a child of a org::kde::kirigami::FormLayout. + * + * It then turns the item specified into a "buddy" + * of the label, making it work as if it were + * a child of the org::kde::kirigami::FormLayout. + * + * A buddy item is useful for instance when the label has a keyboard accelerator, + * which when triggered provides active keyboard focus to the buddy item. + * + * By default buddy is the item that Kirigami.FormData is attached to. + * Custom buddy can only be a direct child of that item; nested components + * are not supported at the moment. + * + * @code + * Kirigami.FormLayout { + * Layouts.ColumnLayout { + * // If the accelerator is in the letter S, + * // pressing Alt+S gives focus to the slider. + * Kirigami.FormData.label: "Slider label:" + * Kirigami.FormData.buddyFor: slider + * + * QQC2.Slider { + * id: slider + * from: 0 + * to: 100 + * value: 50 + * } + * } + * } + * @endcode + */ + Q_PROPERTY(QQuickItem *buddyFor READ buddyFor WRITE setBuddyFor NOTIFY buddyForChanged FINAL) + +public: + explicit FormLayoutAttached(QObject *parent = nullptr); + ~FormLayoutAttached() override; + + void setLabel(const QString &text); + QString label() const; + + void setIsSection(bool section); + bool isSection() const; + + QQuickItem *buddyFor() const; + void setBuddyFor(QQuickItem *aBuddyFor); + + int labelAlignment() const; + void setLabelAlignment(int alignment); + + // QML attached property + static FormLayoutAttached *qmlAttachedProperties(QObject *object); + +Q_SIGNALS: + void labelChanged(); + void isSectionChanged(); + void buddyForChanged(); + void labelAlignmentChanged(); + +private: + void resetBuddyFor(); + + QString m_label; + QString m_actualDecoratedLabel; + QString m_decoratedLabel; + QPointer m_buddyFor; + bool m_isSection = false; + int m_labelAlignment = 0; +}; + +QML_DECLARE_TYPEINFO(FormLayoutAttached, QML_HAS_ATTACHED_PROPERTIES) + +#endif // FORMLAYOUTATTACHED_H diff --git a/src/layouts/headerfooterlayout.cpp b/src/layouts/headerfooterlayout.cpp new file mode 100644 index 0000000..4b7ef3f --- /dev/null +++ b/src/layouts/headerfooterlayout.cpp @@ -0,0 +1,258 @@ +/* + * SPDX-FileCopyrightText: 2023 Marco Martin + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "headerfooterlayout.h" + +#include +#include + +HeaderFooterLayout::HeaderFooterLayout(QQuickItem *parent) + : QQuickItem(parent) + , m_isDirty(false) + , m_performingLayout(false) +{ +} + +HeaderFooterLayout::~HeaderFooterLayout() +{ + disconnectItem(m_header); + disconnectItem(m_contentItem); + disconnectItem(m_footer); +}; + +void HeaderFooterLayout::setHeader(QQuickItem *item) +{ + if (m_header == item) { + return; + } + + if (m_header) { + disconnectItem(m_header); + m_header->setParentItem(nullptr); + } + + m_header = item; + + if (m_header) { + m_header->setParentItem(this); + if (m_header->z() == 0) { + m_header->setZ(1); + } + + connect(m_header, &QQuickItem::implicitWidthChanged, this, &HeaderFooterLayout::markAsDirty); + connect(m_header, &QQuickItem::implicitHeightChanged, this, &HeaderFooterLayout::markAsDirty); + connect(m_header, &QQuickItem::visibleChanged, this, &HeaderFooterLayout::markAsDirty); + + if (m_header->inherits("QQuickTabBar") || m_header->inherits("QQuickToolBar") || m_header->inherits("QQuickDialogButtonBox")) { + // Assume 0 is Header for all 3 types + m_header->setProperty("position", 0); + } + } + + markAsDirty(); + + Q_EMIT headerChanged(); +} + +QQuickItem *HeaderFooterLayout::header() +{ + return m_header; +} + +void HeaderFooterLayout::setContentItem(QQuickItem *item) +{ + if (m_contentItem == item) { + return; + } + + if (m_contentItem) { + disconnectItem(m_contentItem); + m_contentItem->setParentItem(nullptr); + } + + m_contentItem = item; + + if (m_contentItem) { + m_contentItem->setParentItem(this); + connect(m_contentItem, &QQuickItem::implicitWidthChanged, this, &HeaderFooterLayout::markAsDirty); + connect(m_contentItem, &QQuickItem::implicitHeightChanged, this, &HeaderFooterLayout::markAsDirty); + connect(m_contentItem, &QQuickItem::visibleChanged, this, &HeaderFooterLayout::markAsDirty); + } + + markAsDirty(); + + Q_EMIT contentItemChanged(); +} + +QQuickItem *HeaderFooterLayout::contentItem() +{ + return m_contentItem; +} + +void HeaderFooterLayout::setFooter(QQuickItem *item) +{ + if (m_footer == item) { + return; + } + + if (m_footer) { + disconnectItem(m_footer); + m_footer->setParentItem(nullptr); + } + + m_footer = item; + + if (m_footer) { + m_footer->setParentItem(this); + if (m_footer->z() == 0) { + m_footer->setZ(1); + } + + connect(m_footer, &QQuickItem::implicitWidthChanged, this, &HeaderFooterLayout::markAsDirty); + connect(m_footer, &QQuickItem::implicitHeightChanged, this, &HeaderFooterLayout::markAsDirty); + connect(m_footer, &QQuickItem::visibleChanged, this, &HeaderFooterLayout::markAsDirty); + + if (m_footer->inherits("QQuickTabBar") || m_footer->inherits("QQuickToolBar") || m_footer->inherits("QQuickDialogButtonBox")) { + // Assume 1 is Footer for all 3 types + m_footer->setProperty("position", 1); + } + } + + markAsDirty(); + + Q_EMIT footerChanged(); +} + +QQuickItem *HeaderFooterLayout::footer() +{ + return m_footer; +} + +void HeaderFooterLayout::setSpacing(qreal spacing) +{ + if (spacing == m_spacing) { + return; + } + + m_spacing = spacing; + + markAsDirty(); + + Q_EMIT spacingChanged(); +} + +qreal HeaderFooterLayout::spacing() const +{ + return m_spacing; +} + +void HeaderFooterLayout::forceLayout() +{ + updatePolish(); +} + +void HeaderFooterLayout::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + if (newGeometry != oldGeometry) { + markAsDirty(); + } + + QQuickItem::geometryChange(newGeometry, oldGeometry); +} + +void HeaderFooterLayout::componentComplete() +{ + QQuickItem::componentComplete(); + if (m_isDirty) { + performLayout(); + } +} + +void HeaderFooterLayout::updatePolish() +{ + if (m_isDirty) { + performLayout(); + } +} + +void HeaderFooterLayout::markAsDirty() +{ + if (!m_isDirty) { + m_isDirty = true; + polish(); + } +} + +void HeaderFooterLayout::performLayout() +{ + if (!isComponentComplete() || m_performingLayout) { + return; + } + + m_isDirty = false; + m_performingLayout = true; + + // Implicit size has to be updated first, as it may propagate to the + // actual size which will be used below during layouting. + updateImplicitSize(); + + const QSizeF newSize = size(); + qreal headerHeight = 0; + qreal footerHeight = 0; + + if (m_header) { + m_header->setWidth(newSize.width()); + if (m_header->isVisible()) { + headerHeight = m_header->height() + m_spacing; + } + } + if (m_footer) { + m_footer->setY(newSize.height() - m_footer->height()); + m_footer->setWidth(newSize.width()); + if (m_footer->isVisible()) { + footerHeight = m_footer->height() + m_spacing; + } + } + if (m_contentItem) { + m_contentItem->setY(headerHeight); + m_contentItem->setWidth(newSize.width()); + m_contentItem->setHeight(newSize.height() - headerHeight - footerHeight); + } + + m_performingLayout = false; +} + +void HeaderFooterLayout::updateImplicitSize() +{ + qreal impWidth = 0; + qreal impHeight = 0; + + if (m_header && m_header->isVisible()) { + impWidth = std::max(impWidth, m_header->implicitWidth()); + impHeight += m_header->implicitHeight() + m_spacing; + } + if (m_footer && m_footer->isVisible()) { + impWidth = std::max(impWidth, m_footer->implicitWidth()); + impHeight += m_footer->implicitHeight() + m_spacing; + } + if (m_contentItem && m_contentItem->isVisible()) { + impWidth = std::max(impWidth, m_contentItem->implicitWidth()); + impHeight += m_contentItem->implicitHeight(); + } + setImplicitSize(impWidth, impHeight); +} + +void HeaderFooterLayout::disconnectItem(QQuickItem *item) +{ + if (item) { + disconnect(item, &QQuickItem::implicitWidthChanged, this, &HeaderFooterLayout::markAsDirty); + disconnect(item, &QQuickItem::implicitHeightChanged, this, &HeaderFooterLayout::markAsDirty); + disconnect(item, &QQuickItem::visibleChanged, this, &HeaderFooterLayout::markAsDirty); + } +} + +#include "moc_headerfooterlayout.cpp" diff --git a/src/layouts/headerfooterlayout.h b/src/layouts/headerfooterlayout.h new file mode 100644 index 0000000..047ed35 --- /dev/null +++ b/src/layouts/headerfooterlayout.h @@ -0,0 +1,109 @@ +/* + * SPDX-FileCopyrightText: 2023 Marco Martin + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ +#ifndef HEADERFOOTERLAYOUT_H +#define HEADERFOOTERLAYOUT_H + +#include +#include + +/** + * replicates a little part of what Page does, + * It's a container with 3 properties, header, contentItem and footer + * which will be laid out oone on top of each other. It works better than a + * ColumnLayout when the elements are to be defined by properties by the + * user, which would require ugly reparenting dances and container items to + * maintain the layout well behaving. + */ +class HeaderFooterLayout : public QQuickItem +{ + Q_OBJECT + QML_ELEMENT + /** + * @brief This property holds the page header item. + * + * The header item is positioned to the top, + * and resized to the width of the page. The default value is null. + */ + Q_PROPERTY(QQuickItem *header READ header WRITE setHeader NOTIFY headerChanged FINAL) + + /** + * @brief This property holds the visual content Item. + * + * It will be resized both in width and height with the layout resizing. + * Its height will be resized to still have room for the heder and footer + */ + Q_PROPERTY(QQuickItem *contentItem READ contentItem WRITE setContentItem NOTIFY contentItemChanged FINAL) + + /** + * @brief This property holds the page footer item. + * + * The footer item is positioned to the bottom, + * and resized to the width of the page. The default value is null. + */ + Q_PROPERTY(QQuickItem *footer READ footer WRITE setFooter NOTIFY footerChanged FINAL) + + /** + * @brief Space between contentItem and the header and footer items + * + * The content Item of the page will be positioned at this distance in pixels + * from the header and footer Items. The default value is zero. + * + * @since 6.13 + */ + Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged FINAL) + +public: + HeaderFooterLayout(QQuickItem *parent = nullptr); + ~HeaderFooterLayout() override; + + void setHeader(QQuickItem *item); + QQuickItem *header(); + + void setContentItem(QQuickItem *item); + QQuickItem *contentItem(); + + void setFooter(QQuickItem *item); + QQuickItem *footer(); + + void setSpacing(qreal spacing); + qreal spacing() const; + + /** + * @brief HeaderFooterLayout normally positions its header, footer and + * contentItem once per frame (at polish event). This method forces the it + * to recalculate the layout immediately. + */ + Q_INVOKABLE void forceLayout(); + +Q_SIGNALS: + void headerChanged(); + void spacingChanged(); + void contentItemChanged(); + void footerChanged(); + +protected: + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; + void componentComplete() override; + void updatePolish() override; + +private: + void markAsDirty(); + void performLayout(); + void updateImplicitSize(); + void disconnectItem(QQuickItem *item); + + QPointer m_header; + QPointer m_contentItem; + QPointer m_footer; + + qreal m_spacing = 0; + + bool m_isDirty : 1; + bool m_performingLayout : 1; +}; + +#endif diff --git a/src/layouts/padding.cpp b/src/layouts/padding.cpp new file mode 100644 index 0000000..9b1b65c --- /dev/null +++ b/src/layouts/padding.cpp @@ -0,0 +1,472 @@ +/* + * SPDX-FileCopyrightText: 2023 Marco Martin + * SPDX-FileCopyrightText: 2024 Harald Sitter + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "padding.h" + +#include +#include +#include + +class PaddingPrivate +{ + Padding *const q; + +public: + enum Paddings { + Left = 1 << 0, + Top = 1 << 1, + Right = 1 << 2, + Bottom = 1 << 3, + Horizontal = Left | Right, + Vertical = Top | Bottom, + All = Horizontal | Vertical + }; + + PaddingPrivate(Padding *qq) + : q(qq) + { + } + + void calculateImplicitSize(); + void signalPaddings(const QMarginsF &oldPaddings, Paddings paddings); + QMarginsF paddings() const; + void disconnect(); + + QPointer m_contentItem; + + qreal m_padding = 0; + + std::optional m_horizontalPadding; + std::optional m_verticalPadding; + + std::optional m_leftPadding; + std::optional m_topPadding; + std::optional m_rightPadding; + std::optional m_bottomPadding; +}; + +void PaddingPrivate::calculateImplicitSize() +{ + qreal impWidth = 0; + qreal impHeight = 0; + + if (m_contentItem) { + impWidth += m_contentItem->implicitWidth(); + impHeight += m_contentItem->implicitHeight(); + } + + impWidth += q->leftPadding() + q->rightPadding(); + impHeight += q->topPadding() + q->bottomPadding(); + + q->setImplicitSize(impWidth, impHeight); +} + +QMarginsF PaddingPrivate::paddings() const +{ + return {q->leftPadding(), q->topPadding(), q->rightPadding(), q->bottomPadding()}; +} + +void PaddingPrivate::signalPaddings(const QMarginsF &oldPaddings, Paddings which) +{ + if ((which & Left) && !qFuzzyCompare(q->leftPadding(), oldPaddings.left())) { + Q_EMIT q->leftPaddingChanged(); + } + if ((which & Top) && !qFuzzyCompare(q->topPadding(), oldPaddings.top())) { + Q_EMIT q->topPaddingChanged(); + } + if ((which & Right) && !qFuzzyCompare(q->rightPadding(), oldPaddings.right())) { + Q_EMIT q->rightPaddingChanged(); + } + if ((which & Bottom) && !qFuzzyCompare(q->bottomPadding(), oldPaddings.bottom())) { + Q_EMIT q->bottomPaddingChanged(); + } + if ((which == Horizontal || which == All) + && (!qFuzzyCompare(q->leftPadding(), oldPaddings.left()) || !qFuzzyCompare(q->rightPadding(), oldPaddings.right()))) { + Q_EMIT q->horizontalPaddingChanged(); + } + if ((which == Vertical || which == All) + && (!qFuzzyCompare(q->topPadding(), oldPaddings.top()) || !qFuzzyCompare(q->bottomPadding(), oldPaddings.bottom()))) { + Q_EMIT q->verticalPaddingChanged(); + } + if (!qFuzzyCompare(q->leftPadding() + q->rightPadding(), oldPaddings.left() + oldPaddings.right())) { + Q_EMIT q->availableWidthChanged(); + } + if (!qFuzzyCompare(q->topPadding() + q->bottomPadding(), oldPaddings.top() + oldPaddings.bottom())) { + Q_EMIT q->availableHeightChanged(); + } +} + +void PaddingPrivate::disconnect() +{ + if (m_contentItem) { + QObject::disconnect(m_contentItem, &QQuickItem::implicitWidthChanged, q, &Padding::polish); + QObject::disconnect(m_contentItem, &QQuickItem::implicitHeightChanged, q, &Padding::polish); + QObject::disconnect(m_contentItem, &QQuickItem::visibleChanged, q, &Padding::polish); + QObject::disconnect(m_contentItem, &QQuickItem::implicitWidthChanged, q, &Padding::implicitContentWidthChanged); + QObject::disconnect(m_contentItem, &QQuickItem::implicitHeightChanged, q, &Padding::implicitContentHeightChanged); + } +} + +Padding::Padding(QQuickItem *parent) + : QQuickItem(parent) + , d(std::make_unique(this)) +{ +} + +Padding::~Padding() +{ + d->disconnect(); +} + +void Padding::setContentItem(QQuickItem *item) +{ + if (d->m_contentItem == item) { + return; + } + + // Not hiding old contentItem unlike Control, because we can't reliably + // restore it or force `visibile:` binding re-evaluation. + if (d->m_contentItem) { + d->disconnect(); + // Ideally, it should only unset the parent iff old item's parent is + // `this`. But QtQuick.Controls/Control doesn't do that, and we don't + // wanna even more inconsistencies with upstream. + d->m_contentItem->setParentItem(nullptr); + } + + d->m_contentItem = item; + + if (d->m_contentItem) { + d->m_contentItem->setParentItem(this); + connect(d->m_contentItem, &QQuickItem::implicitWidthChanged, this, &Padding::polish); + connect(d->m_contentItem, &QQuickItem::implicitHeightChanged, this, &Padding::polish); + connect(d->m_contentItem, &QQuickItem::visibleChanged, this, &Padding::polish); + connect(d->m_contentItem, &QQuickItem::implicitWidthChanged, this, &Padding::implicitContentWidthChanged); + connect(d->m_contentItem, &QQuickItem::implicitHeightChanged, this, &Padding::implicitContentHeightChanged); + } + + polish(); + + Q_EMIT contentItemChanged(); + Q_EMIT implicitContentWidthChanged(); + Q_EMIT implicitContentWidthChanged(); +} + +QQuickItem *Padding::contentItem() +{ + return d->m_contentItem; +} + +void Padding::setPadding(qreal padding) +{ + if (qFuzzyCompare(padding, d->m_padding)) { + return; + } + + const QMarginsF oldPadding = d->paddings(); + d->m_padding = padding; + + Q_EMIT paddingChanged(); + + d->signalPaddings(oldPadding, PaddingPrivate::All); + + polish(); +} + +void Padding::resetPadding() +{ + if (qFuzzyCompare(d->m_padding, 0)) { + return; + } + + const QMarginsF oldPadding = d->paddings(); + d->m_padding = 0; + + Q_EMIT paddingChanged(); + + d->signalPaddings(oldPadding, PaddingPrivate::All); + + polish(); +} + +qreal Padding::padding() const +{ + return d->m_padding; +} + +void Padding::setHorizontalPadding(qreal padding) +{ + if (qFuzzyCompare(padding, horizontalPadding()) && d->m_horizontalPadding.has_value()) { + return; + } + + const QMarginsF oldPadding = d->paddings(); + d->m_horizontalPadding = padding; + + d->signalPaddings(oldPadding, PaddingPrivate::Horizontal); + + polish(); +} + +void Padding::resetHorizontalPadding() +{ + if (!d->m_horizontalPadding.has_value()) { + return; + } + + const QMarginsF oldPadding = d->paddings(); + d->m_horizontalPadding.reset(); + + d->signalPaddings(oldPadding, PaddingPrivate::Horizontal); + + polish(); +} + +qreal Padding::horizontalPadding() const +{ + return d->m_horizontalPadding.value_or(d->m_padding); +} + +void Padding::setVerticalPadding(qreal padding) +{ + if (qFuzzyCompare(padding, verticalPadding()) && d->m_verticalPadding.has_value()) { + return; + } + + const QMarginsF oldPadding = d->paddings(); + d->m_verticalPadding = padding; + + d->signalPaddings(oldPadding, PaddingPrivate::Vertical); + + polish(); +} + +void Padding::resetVerticalPadding() +{ + if (!d->m_verticalPadding.has_value()) { + return; + } + + const QMarginsF oldPadding = d->paddings(); + d->m_verticalPadding.reset(); + + d->signalPaddings(oldPadding, PaddingPrivate::Vertical); + + polish(); +} + +qreal Padding::verticalPadding() const +{ + return d->m_verticalPadding.value_or(d->m_padding); +} + +void Padding::setLeftPadding(qreal padding) +{ + const QMarginsF oldPadding = d->paddings(); + if (qFuzzyCompare(padding, oldPadding.left()) && d->m_leftPadding.has_value()) { + return; + } + + d->m_leftPadding = padding; + + d->signalPaddings(oldPadding, PaddingPrivate::Left); + + polish(); +} + +void Padding::resetLeftPadding() +{ + if (!d->m_leftPadding.has_value()) { + return; + } + + const QMarginsF oldPadding = d->paddings(); + d->m_leftPadding.reset(); + + d->signalPaddings(oldPadding, PaddingPrivate::Left); + + polish(); +} + +qreal Padding::leftPadding() const +{ + if (d->m_leftPadding.has_value()) { + return d->m_leftPadding.value(); + } else { + return horizontalPadding(); + } +} + +void Padding::setTopPadding(qreal padding) +{ + const QMarginsF oldPadding = d->paddings(); + if (qFuzzyCompare(padding, oldPadding.top()) && d->m_topPadding.has_value()) { + return; + } + + d->m_topPadding = padding; + + d->signalPaddings(oldPadding, PaddingPrivate::Top); + + polish(); +} + +void Padding::resetTopPadding() +{ + if (!d->m_topPadding.has_value()) { + return; + } + + const QMarginsF oldPadding = d->paddings(); + d->m_topPadding.reset(); + + d->signalPaddings(oldPadding, PaddingPrivate::Top); + + polish(); +} + +qreal Padding::topPadding() const +{ + if (d->m_topPadding.has_value()) { + return d->m_topPadding.value(); + } else { + return verticalPadding(); + } +} + +void Padding::setRightPadding(qreal padding) +{ + const QMarginsF oldPadding = d->paddings(); + if (qFuzzyCompare(padding, oldPadding.right()) && d->m_rightPadding.has_value()) { + return; + } + + d->m_rightPadding = padding; + + d->signalPaddings(oldPadding, PaddingPrivate::Right); + + polish(); +} + +void Padding::resetRightPadding() +{ + if (!d->m_rightPadding.has_value()) { + return; + } + + const QMarginsF oldPadding = d->paddings(); + d->m_rightPadding.reset(); + + d->signalPaddings(oldPadding, PaddingPrivate::Right); + + polish(); +} + +qreal Padding::rightPadding() const +{ + if (d->m_rightPadding.has_value()) { + return d->m_rightPadding.value(); + } else { + return horizontalPadding(); + } +} + +void Padding::setBottomPadding(qreal padding) +{ + const QMarginsF oldPadding = d->paddings(); + if (qFuzzyCompare(padding, oldPadding.bottom()) && d->m_bottomPadding.has_value()) { + return; + } + + d->m_bottomPadding = padding; + + d->signalPaddings(oldPadding, PaddingPrivate::Bottom); + + polish(); +} + +void Padding::resetBottomPadding() +{ + if (!d->m_bottomPadding.has_value()) { + return; + } + + const QMarginsF oldPadding = d->paddings(); + d->m_bottomPadding.reset(); + + d->signalPaddings(oldPadding, PaddingPrivate::Bottom); + + polish(); +} + +qreal Padding::bottomPadding() const +{ + if (d->m_bottomPadding.has_value()) { + return d->m_bottomPadding.value(); + } else { + return verticalPadding(); + } +} + +qreal Padding::availableWidth() const +{ + return width() - leftPadding() - rightPadding(); +} + +qreal Padding::availableHeight() const +{ + return height() - topPadding() - bottomPadding(); +} + +qreal Padding::implicitContentWidth() const +{ + if (d->m_contentItem) { + return d->m_contentItem->implicitWidth(); + } else { + return 0.0; + } +} + +qreal Padding::implicitContentHeight() const +{ + if (d->m_contentItem) { + return d->m_contentItem->implicitHeight(); + } else { + return 0.0; + } +} + +void Padding::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + if (newGeometry != oldGeometry) { + Q_EMIT availableWidthChanged(); + Q_EMIT availableHeightChanged(); + polish(); + } + + QQuickItem::geometryChange(newGeometry, oldGeometry); +} + +void Padding::updatePolish() +{ + d->calculateImplicitSize(); + if (!d->m_contentItem) { + return; + } + + d->m_contentItem->setPosition(QPointF(leftPadding(), topPadding())); + d->m_contentItem->setSize(QSizeF(availableWidth(), availableHeight())); +} + +void Padding::componentComplete() +{ + QQuickItem::componentComplete(); + // This is important! We must have a geometry so our parents can lay out. + updatePolish(); +} + +#include "moc_padding.cpp" diff --git a/src/layouts/padding.h b/src/layouts/padding.h new file mode 100644 index 0000000..b23c3d3 --- /dev/null +++ b/src/layouts/padding.h @@ -0,0 +1,213 @@ +/* + * SPDX-FileCopyrightText: 2023 Marco Martin + * SPDX-FileCopyrightText: 2024 Harald Sitter + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ +#ifndef PADDING_H +#define PADDING_H + +#include +#include + +class PaddingPrivate; + +/** + * This item simply adds an external padding to contentItem's size. + * + * Padding item behaves similarly to QtQuick.Controls/Control::padding, + * but is more lightweight and thus efficient. Its implicit size is set + * to that of its contentItem's implicit size plus padding. + * + * @code + * import QtQuick.Controls as QQC2 + * import org.kde.kirigami as Kirigami + * + * Kirigami.Padding { + * padding: Kirigami.Units.largeSpacing + * contentItem: QQC2.Button {} + * } + * @endcode + * + * With this component it is possible to add external paddings as a + * placeholder for an item, whereas with QtQuick.Layouts you would need to + * manually assign or bind attached properties whenever content item changes. + * + * @code + * import QtQuick + * import QtQuick.Layouts + * import QtQuick.Controls as QQC2 + * import org.kde.kirigami as Kirigami + * + * ColumnLayout { + * property alias contentItem: container.contentItem + * + * Kirigami.Heading { + * Layout.fillWidth: true + * Layout.margins: Kirigami.Units.largeSpacing + * } + * + * Kirigami.Padding { + * id: container + * Layout.fillWidth: true + * padding: Kirigami.Units.largeSpacing + * } + * } + * @endcode + * + * @since KDE Frameworks 6.0 + */ +class Padding : public QQuickItem +{ + Q_OBJECT + QML_ELEMENT + + /** + * @brief This property holds the visual content Item. + * + * It will automatically resized taking into account all the paddings + */ + Q_PROPERTY(QQuickItem *contentItem READ contentItem WRITE setContentItem NOTIFY contentItemChanged FINAL) + + /** + * @brief This property holds the default padding. + * + * Padding adds a space between each edge of this ITem and its contentItem, effectively controlling its size. + * To specify a padding value for a specific edge of the control, set its relevant property: + * * leftPadding + * * rightPadding + * * topPadding + * * bottomPadding + */ + Q_PROPERTY(qreal padding READ padding WRITE setPadding NOTIFY paddingChanged RESET resetPadding FINAL) + + /** + * @brief This property holds the horizontal padding. + * + * Unless explicitly set, the value is equal to padding. + */ + Q_PROPERTY(qreal horizontalPadding READ horizontalPadding WRITE setHorizontalPadding NOTIFY horizontalPaddingChanged RESET resetHorizontalPadding FINAL) + + /** + * @brief This property holds the vertical padding. + * + * Unless explicitly set, the value is equal to padding. + */ + Q_PROPERTY(qreal verticalPadding READ verticalPadding WRITE setVerticalPadding NOTIFY verticalPaddingChanged RESET resetVerticalPadding FINAL) + + /** + * @brief This property holds the padding on the left side. + * + * Unless explicitly set, it falls back to horizontalPadding and then to padding. + * This always refers to the actual left, it won't be flipped on RTL layouts. + */ + Q_PROPERTY(qreal leftPadding READ leftPadding WRITE setLeftPadding NOTIFY leftPaddingChanged RESET resetLeftPadding FINAL) + + /** + * @brief the padding on the top side. + * + * Unless explicitly set, it falls back to verticalPadding and then to padding. + */ + Q_PROPERTY(qreal topPadding READ topPadding WRITE setTopPadding NOTIFY topPaddingChanged RESET resetTopPadding FINAL) + + /** + * @brief This property holds the padding on the right side. + * + * Unless explicitly set, it falls back to horizontalPadding and then to padding. + * This always refers to the actual right, it won't be flipped on RTL layouts. + */ + Q_PROPERTY(qreal rightPadding READ rightPadding WRITE setRightPadding NOTIFY rightPaddingChanged RESET resetRightPadding FINAL) + + /** + * @brief This property holds the padding on the bottom side. + * + * Unless explicitly set, it falls back to verticalPadding and then to padding. + */ + Q_PROPERTY(qreal bottomPadding READ bottomPadding WRITE setBottomPadding NOTIFY bottomPaddingChanged RESET resetBottomPadding FINAL) + + /** + * @brief The width available to the contentItem after deducting horizontal padding from the width of the Padding. + */ + Q_PROPERTY(qreal availableWidth READ availableWidth NOTIFY availableWidthChanged FINAL) + + /** + * @brief The height available to the contentItem after deducting vertical padding from the width of the Padding. + */ + Q_PROPERTY(qreal availableHeight READ availableHeight NOTIFY availableHeightChanged FINAL) + + /** + * @brief The implicitWidth of its contentItem, or 0 if not present. + */ + Q_PROPERTY(qreal implicitContentWidth READ implicitContentWidth NOTIFY implicitContentWidthChanged FINAL) + + /** + * @brief The implicitHeight of its contentItem, or 0 if not present. + */ + Q_PROPERTY(qreal implicitContentHeight READ implicitContentHeight NOTIFY implicitContentHeightChanged FINAL) + +public: + Padding(QQuickItem *parent = nullptr); + ~Padding() override; + + void setContentItem(QQuickItem *item); + QQuickItem *contentItem(); + + void setPadding(qreal padding); + void resetPadding(); + qreal padding() const; + + void setHorizontalPadding(qreal padding); + void resetHorizontalPadding(); + qreal horizontalPadding() const; + + void setVerticalPadding(qreal padding); + void resetVerticalPadding(); + qreal verticalPadding() const; + + void setLeftPadding(qreal padding); + void resetLeftPadding(); + qreal leftPadding() const; + + void setTopPadding(qreal padding); + void resetTopPadding(); + qreal topPadding() const; + + void setRightPadding(qreal padding); + void resetRightPadding(); + qreal rightPadding() const; + + void setBottomPadding(qreal padding); + void resetBottomPadding(); + qreal bottomPadding() const; + + qreal availableWidth() const; + qreal availableHeight() const; + + qreal implicitContentWidth() const; + qreal implicitContentHeight() const; + +Q_SIGNALS: + void contentItemChanged(); + void paddingChanged(); + void horizontalPaddingChanged(); + void verticalPaddingChanged(); + void leftPaddingChanged(); + void topPaddingChanged(); + void rightPaddingChanged(); + void bottomPaddingChanged(); + void availableHeightChanged(); + void availableWidthChanged(); + void implicitContentWidthChanged(); + void implicitContentHeightChanged(); + +protected: + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; + void updatePolish() override; + void componentComplete() override; + +private: + friend class PaddingPrivate; + const std::unique_ptr d; +}; + +#endif diff --git a/src/layouts/pagestackattached.cpp b/src/layouts/pagestackattached.cpp new file mode 100644 index 0000000..cae6af2 --- /dev/null +++ b/src/layouts/pagestackattached.cpp @@ -0,0 +1,240 @@ +// SPDX-FileCopyrightText: 2024 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "pagestackattached.h" + +#include "formlayoutattached.h" +#include "loggingcategory.h" + +#include +#include +#include + +using namespace Qt::StringLiterals; + +template +bool callIfValid(QObject *object, const char *method, Args &&...args) +{ + auto metaObject = object->metaObject(); + auto index = metaObject->indexOfMethod(method); + if (index != -1) { + auto method = metaObject->method(index); + return method.invoke(object, args...); + } + + return false; +} + +bool tryCall(QObject *object, QByteArrayView pageRowMethod, QByteArrayView stackViewMethod, const QVariant &page, const QVariantMap &properties) +{ + const auto metaObject = object->metaObject(); + + QByteArray name = pageRowMethod + "(QVariant,QVariant)"; + if (auto index = metaObject->indexOfMethod(name.data()); index != -1) { + return metaObject->method(index).invoke(object, page, QVariant::fromValue(properties)); + } else if (QQmlComponent *component = page.value(); component != nullptr) { + return metaObject->invokeMethod(object, stackViewMethod.data(), component, properties); + } else if (QQuickItem *item = page.value(); item != nullptr) { + return metaObject->invokeMethod(object, stackViewMethod.data(), item, properties); + } else if (QUrl url = page.toUrl(); !url.isEmpty()) { + return metaObject->invokeMethod(object, stackViewMethod.data(), url, properties); + } + + return false; +} + +PageStackAttached::PageStackAttached(QObject *parent) + : QQuickAttachedPropertyPropagator(parent) +{ + m_parentItem = qobject_cast(parent); + + if (!m_parentItem) { + qCDebug(KirigamiLayoutsLog) << "PageStack must be attached to an Item" << parent; + return; + } + + if (hasStackCapabilities(m_parentItem)) { + setPageStack(m_parentItem); + } else if (!m_pageStack) { + QQuickItem *candidate = m_parentItem->parentItem(); + while (candidate) { + if (hasStackCapabilities(candidate)) { + qmlAttachedPropertiesObject(candidate, true); + break; + } + candidate = candidate->parentItem(); + } + } + + initialize(); +} + +QQuickItem *PageStackAttached::pageStack() const +{ + return m_pageStack; +} + +void PageStackAttached::setPageStack(QQuickItem *pageStack) +{ + if (!pageStack || m_pageStack == pageStack || !hasStackCapabilities(pageStack)) { + return; + } + + m_customStack = true; + m_pageStack = pageStack; + + propagatePageStack(pageStack); + + Q_EMIT pageStackChanged(); +} + +void PageStackAttached::propagatePageStack(QQuickItem *pageStack) +{ + if (!pageStack) { + return; + } + + if (!m_customStack && m_pageStack != pageStack) { + m_pageStack = pageStack; + Q_EMIT pageStackChanged(); + } + + const auto stacks = attachedChildren(); + for (QQuickAttachedPropertyPropagator *child : stacks) { + PageStackAttached *stackAttached = qobject_cast(child); + if (stackAttached) { + stackAttached->propagatePageStack(m_pageStack); + } + } +} + +void PageStackAttached::push(const QVariant &page, const QVariantMap &properties) +{ + if (!m_pageStack) { + qCWarning(KirigamiLayoutsLog) << "Pushing in an empty PageStackAttached"; + return; + } + + if (!tryCall(m_pageStack, "push", "pushItem", page, properties)) { + qCWarning(KirigamiLayoutsLog) << "Invalid parameters to push: " << page << properties; + } +} + +void PageStackAttached::replace(const QVariant &page, const QVariantMap &properties) +{ + if (!m_pageStack) { + qCWarning(KirigamiLayoutsLog) << "replacing in an empty PageStackAttached"; + return; + } + + if (!tryCall(m_pageStack, "replace", "replaceCurrentItem", page, properties)) { + qCWarning(KirigamiLayoutsLog) << "Invalid parameters to replace: " << page << properties; + } +} + +void PageStackAttached::pop(const QVariant &page) +{ + if (!m_pageStack) { + qCWarning(KirigamiLayoutsLog) << "Pushing in an empty PageStackAttached"; + return; + } + + if (callIfValid(m_pageStack, "pop(QVariant)", page)) { + return; + } else if (page.canConvert() && callIfValid(m_pageStack, "popToItem(QQuickItem*)", page.value())) { + return; + } else if (callIfValid(m_pageStack, "popCurrentItem()")) { + return; + } + + qCWarning(KirigamiLayoutsLog) << "Pop operation failed on stack" << m_pageStack << "with page" << page; +} + +void PageStackAttached::clear() +{ + if (!m_pageStack) { + qCWarning(KirigamiLayoutsLog) << "Clearing in an empty PageStackAttached"; + return; + } + + if (!callIfValid(m_pageStack, "clear()")) { + qCWarning(KirigamiLayoutsLog) << "Call to clear() failed"; + } +} + +bool PageStackAttached::hasStackCapabilities(QQuickItem *candidate) +{ + // Duck type the candidate in order to see if it can be used as a stack having the expected methods + auto metaObject = candidate->metaObject(); + Q_ASSERT(metaObject); + + auto hasPageRowOrStackViewMethod = [metaObject](QByteArrayView pageRowMethod, QByteArrayView stackViewMethod) -> bool { + // For PageRow, we require a single method that takes QVariant,QVariant as argument. + QByteArray name = pageRowMethod + "(QVariant,QVariant)"; + if (metaObject->indexOfMethod(name.data()) != -1) { + return true; + } + + // For StackView, we require three variants of the method. + name = stackViewMethod + "(QQmlComponent*,QVariantMap)"; + if (metaObject->indexOfMethod(name.data()) == -1) { + return false; + } + + name = stackViewMethod + "(QQuickItem*,QVariantMap)"; + if (metaObject->indexOfMethod(name.data()) == -1) { + return false; + } + + name = stackViewMethod + "(QUrl,QVariantMap)"; + if (metaObject->indexOfMethod(name.data()) == -1) { + return false; + } + + return true; + }; + + if (!hasPageRowOrStackViewMethod("push", "pushItem")) { + return false; + } + + if (!hasPageRowOrStackViewMethod("replace", "replaceCurrentItem")) { + return false; + } + + auto index = metaObject->indexOfMethod("pop(QVariant)"); + if (index == -1) { + index = metaObject->indexOfMethod("popToItem(QQuickItem*)"); + if (index == -1) { + return false; + } + index = metaObject->indexOfMethod("popCurrentItem()"); + if (index == -1) { + return false; + } + } + + index = metaObject->indexOfMethod("clear()"); + if (index == -1) { + return false; + } + + return true; +} + +PageStackAttached *PageStackAttached::qmlAttachedProperties(QObject *object) +{ + return new PageStackAttached(object); +} + +void PageStackAttached::attachedParentChange(QQuickAttachedPropertyPropagator *newParent, QQuickAttachedPropertyPropagator *oldParent) +{ + Q_UNUSED(oldParent); + + PageStackAttached *stackAttached = qobject_cast(newParent); + if (stackAttached) { + propagatePageStack(stackAttached->pageStack()); + } +} + +#include "moc_pagestackattached.cpp" diff --git a/src/layouts/pagestackattached.h b/src/layouts/pagestackattached.h new file mode 100644 index 0000000..8c94444 --- /dev/null +++ b/src/layouts/pagestackattached.h @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2024 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include +#include +#include + +/** + * This attached property makes possible to access from anywhere the + * page stack this page was pushed into. + * It can be an instance of org::kde::kirigami::PageRow or + * a StackView from QtQuickControls + * + * Kirigami.Page { + * id: root + * + * Button { + * text: "Push Page" + * onClicked: Kirigami.PageStack.push(Qt.resolvedurl("AnotherPage")); + * } + * } + * + * @since 6.10 + */ +class PageStackAttached : public QQuickAttachedPropertyPropagator +{ + Q_OBJECT + QML_NAMED_ELEMENT(PageStack) + QML_ATTACHED(PageStackAttached) + QML_UNCREATABLE("") + + Q_PROPERTY(QQuickItem *pageStack READ pageStack WRITE setPageStack NOTIFY pageStackChanged) + +public: + explicit PageStackAttached(QObject *parent); + + /*! + \qmlattachedproperty PageRow PageStack::pageStack + + This property holds the pageStack where this page was pushed. + It will point to the proper instance in the parent hyerarchy + and normally is not necessary to explicitly write it. + Write on this property only if it's desired this attached + property and those of all the children to point to a different + PageRow or StackView + */ + QQuickItem *pageStack() const; + void setPageStack(QQuickItem *pageStack); + + Q_INVOKABLE void push(const QVariant &page, const QVariantMap &properties = QVariantMap()); + Q_INVOKABLE void replace(const QVariant &page, const QVariantMap &properties = QVariantMap()); + Q_INVOKABLE void pop(const QVariant &page = QVariant()); + Q_INVOKABLE void clear(); + + static PageStackAttached *qmlAttachedProperties(QObject *object); + +protected: + bool hasStackCapabilities(QQuickItem *candidate); + void propagatePageStack(QQuickItem *pageStack); + void attachedParentChange(QQuickAttachedPropertyPropagator *newParent, QQuickAttachedPropertyPropagator *oldParent) override; + +Q_SIGNALS: + void pageStackChanged(); + +private: + QPointer m_pageStack; + QPointer m_parentItem; + bool m_customStack = false; +}; diff --git a/src/layouts/sizegroup.cpp b/src/layouts/sizegroup.cpp new file mode 100644 index 0000000..293cb62 --- /dev/null +++ b/src/layouts/sizegroup.cpp @@ -0,0 +1,129 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include + +#include "sizegroup.h" + +#define pThis (static_cast(prop->object)) + +void SizeGroup::appendItem(QQmlListProperty *prop, QQuickItem *value) +{ + pThis->m_items << value; + pThis->connectItem(value); +} + +qsizetype SizeGroup::itemCount(QQmlListProperty *prop) +{ + return pThis->m_items.count(); +} + +QQuickItem *SizeGroup::itemAt(QQmlListProperty *prop, qsizetype index) +{ + return pThis->m_items[index]; +} + +void SizeGroup::clearItems(QQmlListProperty *prop) +{ + for (const auto &item : std::as_const(pThis->m_items)) { + QObject::disconnect(pThis->m_connections[item].first); + QObject::disconnect(pThis->m_connections[item].second); + } + pThis->m_items.clear(); +} + +void SizeGroup::connectItem(QQuickItem *item) +{ + auto conn1 = connect(item, &QQuickItem::implicitWidthChanged, this, [this]() { + adjustItems(Mode::Width); + }); + auto conn2 = connect(item, &QQuickItem::implicitHeightChanged, this, [this]() { + adjustItems(Mode::Height); + }); + m_connections[item] = qMakePair(conn1, conn2); + adjustItems(m_mode); +} + +QQmlListProperty SizeGroup::items() +{ + return QQmlListProperty(this, // + nullptr, + appendItem, + itemCount, + itemAt, + clearItems); +} + +void SizeGroup::relayout() +{ + adjustItems(Mode::Both); +} + +void SizeGroup::componentComplete() +{ + adjustItems(Mode::Both); +} + +void SizeGroup::adjustItems(Mode whatChanged) +{ + if (m_mode == Mode::Width && whatChanged == Mode::Height) { + return; + } + if (m_mode == Mode::Height && whatChanged == Mode::Width) { + return; + } + + qreal maxHeight = 0.0; + qreal maxWidth = 0.0; + + for (const auto &item : std::as_const(m_items)) { + if (item == nullptr) { + continue; + } + + switch (m_mode) { + case Mode::Width: + maxWidth = qMax(maxWidth, item->implicitWidth()); + break; + case Mode::Height: + maxHeight = qMax(maxHeight, item->implicitHeight()); + break; + case Mode::Both: + maxWidth = qMax(maxWidth, item->implicitWidth()); + maxHeight = qMax(maxHeight, item->implicitHeight()); + break; + case Mode::None: + break; + } + } + + for (const auto &item : std::as_const(m_items)) { + if (item == nullptr) { + continue; + } + + if (!qmlEngine(item) || !qmlContext(item)) { + continue; + } + + switch (m_mode) { + case Mode::Width: + QQmlProperty(item, QStringLiteral("Layout.preferredWidth"), qmlContext(item)).write(maxWidth); + break; + case Mode::Height: + QQmlProperty(item, QStringLiteral("Layout.preferredHeight"), qmlContext(item)).write(maxHeight); + break; + case Mode::Both: + QQmlProperty(item, QStringLiteral("Layout.preferredWidth"), qmlContext(item)).write(maxWidth); + QQmlProperty(item, QStringLiteral("Layout.preferredHeight"), qmlContext(item)).write(maxHeight); + break; + case Mode::None: + break; + } + } +} + +#include "moc_sizegroup.cpp" diff --git a/src/layouts/sizegroup.h b/src/layouts/sizegroup.h new file mode 100644 index 0000000..6677680 --- /dev/null +++ b/src/layouts/sizegroup.h @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +/** + * SizeGroup is a utility object that makes groups of items request the same size. + */ +class SizeGroup : public QObject, public QQmlParserStatus +{ + Q_OBJECT + QML_ELEMENT + Q_INTERFACES(QQmlParserStatus) + +public: + enum Mode { + None = 0, /// SizeGroup does nothing + Width = 1, /// SizeGroup syncs item widths + Height = 2, /// SizeGroup syncs item heights + Both = 3, /// SizeGroup syncs both item widths and heights + }; + Q_ENUM(Mode) + Q_DECLARE_FLAGS(Modes, Mode) + +private: + Mode m_mode = None; + QList> m_items; + QMap> m_connections; + +public: + /** + * Which dimensions this SizeGroup should adjust + */ + Q_PROPERTY(Mode mode MEMBER m_mode NOTIFY modeChanged FINAL) + Q_SIGNAL void modeChanged(); + + /** + * Which items this SizeGroup should adjust + */ + Q_PROPERTY(QQmlListProperty items READ items CONSTANT FINAL) + QQmlListProperty items(); + + void adjustItems(Mode whatChanged); + void connectItem(QQuickItem *item); + + /** + * Forces the SizeGroup to relayout items. + * + * Normally this is never needed as the SizeGroup automatically + * relayout items as they're added and their sizes change. + */ + Q_INVOKABLE void relayout(); + + void classBegin() override + { + } + void componentComplete() override; + +private: + static void appendItem(QQmlListProperty *prop, QQuickItem *value); + static qsizetype itemCount(QQmlListProperty *prop); + static QQuickItem *itemAt(QQmlListProperty *prop, qsizetype index); + static void clearItems(QQmlListProperty *prop); +}; diff --git a/src/layouts/toolbarlayout.cpp b/src/layouts/toolbarlayout.cpp new file mode 100644 index 0000000..a4ef082 --- /dev/null +++ b/src/layouts/toolbarlayout.cpp @@ -0,0 +1,795 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +#include "toolbarlayout.h" + +#include +#include + +#include +#include +#include +#include + +#include "loggingcategory.h" +#include "toolbarlayoutdelegate.h" + +ToolBarLayoutAttached::ToolBarLayoutAttached(QObject *parent) + : QObject(parent) +{ +} + +QObject *ToolBarLayoutAttached::action() const +{ + return m_action; +} + +void ToolBarLayoutAttached::setAction(QObject *action) +{ + m_action = action; +} + +class ToolBarLayoutPrivate +{ + ToolBarLayout *const q; + +public: + ToolBarLayoutPrivate(ToolBarLayout *qq) + : q(qq) + { + } + ~ToolBarLayoutPrivate() + { + if (moreButtonIncubator) { + moreButtonIncubator->clear(); + delete moreButtonIncubator; + } + } + + void calculateImplicitSize(); + void performLayout(); + QList createDelegates(); + ToolBarLayoutDelegate *createDelegate(QObject *action); + qreal layoutStart(qreal layoutWidth); + void maybeHideDelegate(int index, qreal ¤tWidth, qreal totalWidth); + + QList actions; + ToolBarLayout::ActionsProperty actionsProperty; + QList hiddenActions; + QQmlComponent *fullDelegate = nullptr; + QQmlComponent *iconDelegate = nullptr; + QQmlComponent *separatorDelegate = nullptr; + QQmlComponent *moreButton = nullptr; + qreal spacing = 0.0; + Qt::Alignment alignment = Qt::AlignLeft; + qreal visibleActionsWidth = 0.0; + qreal visibleWidth = 0.0; + Qt::LayoutDirection layoutDirection = Qt::LeftToRight; + ToolBarLayout::HeightMode heightMode = ToolBarLayout::ConstrainIfLarger; + + bool completed = false; + bool actionsChanged = false; + bool implicitSizeValid = false; + + std::unordered_map> delegates; + QList sortedDelegates; + QQuickItem *moreButtonInstance = nullptr; + ToolBarDelegateIncubator *moreButtonIncubator = nullptr; + bool shouldShowMoreButton = false; + int firstHiddenIndex = -1; + + QList removedActions; + QTimer *removalTimer = nullptr; + + QElapsedTimer performanceTimer; + + static void appendAction(ToolBarLayout::ActionsProperty *list, QObject *action); + static qsizetype actionCount(ToolBarLayout::ActionsProperty *list); + static QObject *action(ToolBarLayout::ActionsProperty *list, qsizetype index); + static void clearActions(ToolBarLayout::ActionsProperty *list); +}; + +ToolBarLayout::ToolBarLayout(QQuickItem *parent) + : QQuickItem(parent) + , d(std::make_unique(this)) +{ + d->actionsProperty = ActionsProperty(this, + this, + ToolBarLayoutPrivate::appendAction, + ToolBarLayoutPrivate::actionCount, + ToolBarLayoutPrivate::action, + ToolBarLayoutPrivate::clearActions); + + // To prevent multiple assignments to actions from constantly recreating + // delegates, we cache the delegates and only remove them once they are no + // longer being used. This timer is responsible for triggering that removal. + d->removalTimer = new QTimer{this}; + d->removalTimer->setInterval(1000); + d->removalTimer->setSingleShot(true); + connect(d->removalTimer, &QTimer::timeout, this, [this]() { + for (auto action : std::as_const(d->removedActions)) { + if (!d->actions.contains(action)) { + d->delegates.erase(action); + } + } + d->removedActions.clear(); + }); +} + +ToolBarLayout::~ToolBarLayout() +{ +} + +ToolBarLayout::ActionsProperty ToolBarLayout::actionsProperty() const +{ + return d->actionsProperty; +} + +void ToolBarLayout::addAction(QObject *action) +{ + if (action == nullptr) { + return; + } + d->actions.append(action); + d->actionsChanged = true; + + connect(action, &QObject::destroyed, this, [this](QObject *action) { + auto itr = d->delegates.find(action); + if (itr != d->delegates.end()) { + d->delegates.erase(itr); + } + + d->actions.removeOne(action); + d->actionsChanged = true; + + relayout(); + }); + + relayout(); +} + +void ToolBarLayout::removeAction(QObject *action) +{ + auto itr = d->delegates.find(action); + if (itr != d->delegates.end()) { + itr->second->hide(); + } + + d->actions.removeOne(action); + d->removedActions.append(action); + d->removalTimer->start(); + d->actionsChanged = true; + + relayout(); +} + +void ToolBarLayout::clearActions() +{ + for (auto action : std::as_const(d->actions)) { + auto itr = d->delegates.find(action); + if (itr != d->delegates.end()) { + itr->second->hide(); + } + } + + d->removedActions.append(d->actions); + d->actions.clear(); + d->actionsChanged = true; + + relayout(); +} + +QList ToolBarLayout::hiddenActions() const +{ + return d->hiddenActions; +} + +QQmlComponent *ToolBarLayout::fullDelegate() const +{ + return d->fullDelegate; +} + +void ToolBarLayout::setFullDelegate(QQmlComponent *newFullDelegate) +{ + if (newFullDelegate == d->fullDelegate) { + return; + } + + d->fullDelegate = newFullDelegate; + d->delegates.clear(); + relayout(); + Q_EMIT fullDelegateChanged(); +} + +QQmlComponent *ToolBarLayout::iconDelegate() const +{ + return d->iconDelegate; +} + +void ToolBarLayout::setIconDelegate(QQmlComponent *newIconDelegate) +{ + if (newIconDelegate == d->iconDelegate) { + return; + } + + d->iconDelegate = newIconDelegate; + d->delegates.clear(); + relayout(); + Q_EMIT iconDelegateChanged(); +} + +QQmlComponent *ToolBarLayout::separatorDelegate() const +{ + return d->separatorDelegate; +} + +void ToolBarLayout::setSeparatorDelegate(QQmlComponent *newSeparatorDelegate) +{ + if (newSeparatorDelegate == d->separatorDelegate) { + return; + } + + d->separatorDelegate = newSeparatorDelegate; + d->delegates.clear(); + relayout(); + Q_EMIT separatorDelegateChanged(); +} + +QQmlComponent *ToolBarLayout::moreButton() const +{ + return d->moreButton; +} + +void ToolBarLayout::setMoreButton(QQmlComponent *newMoreButton) +{ + if (newMoreButton == d->moreButton) { + return; + } + + d->moreButton = newMoreButton; + if (d->moreButtonInstance) { + d->moreButtonInstance->deleteLater(); + d->moreButtonInstance = nullptr; + } + relayout(); + Q_EMIT moreButtonChanged(); +} + +qreal ToolBarLayout::spacing() const +{ + return d->spacing; +} + +void ToolBarLayout::setSpacing(qreal newSpacing) +{ + if (newSpacing == d->spacing) { + return; + } + + d->spacing = newSpacing; + relayout(); + Q_EMIT spacingChanged(); +} + +Qt::Alignment ToolBarLayout::alignment() const +{ + return d->alignment; +} + +void ToolBarLayout::setAlignment(Qt::Alignment newAlignment) +{ + if (newAlignment == d->alignment) { + return; + } + + d->alignment = newAlignment; + relayout(); + Q_EMIT alignmentChanged(); +} + +qreal ToolBarLayout::visibleWidth() const +{ + return d->visibleWidth; +} + +qreal ToolBarLayout::minimumWidth() const +{ + return d->moreButtonInstance ? d->moreButtonInstance->width() : 0; +} + +Qt::LayoutDirection ToolBarLayout::layoutDirection() const +{ + return d->layoutDirection; +} + +void ToolBarLayout::setLayoutDirection(Qt::LayoutDirection &newLayoutDirection) +{ + if (newLayoutDirection == d->layoutDirection) { + return; + } + + d->layoutDirection = newLayoutDirection; + relayout(); + Q_EMIT layoutDirectionChanged(); +} + +ToolBarLayout::HeightMode ToolBarLayout::heightMode() const +{ + return d->heightMode; +} + +void ToolBarLayout::setHeightMode(HeightMode newHeightMode) +{ + if (newHeightMode == d->heightMode) { + return; + } + + d->heightMode = newHeightMode; + relayout(); + Q_EMIT heightModeChanged(); +} + +void ToolBarLayout::relayout() +{ + d->implicitSizeValid = false; + polish(); +} + +void ToolBarLayout::componentComplete() +{ + QQuickItem::componentComplete(); + d->completed = true; + relayout(); +} + +void ToolBarLayout::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + if (newGeometry != oldGeometry) { + if (newGeometry.size() != QSizeF{implicitWidth(), implicitHeight()}) { + relayout(); + } else { + polish(); + } + } + QQuickItem::geometryChange(newGeometry, oldGeometry); +} + +void ToolBarLayout::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) +{ + if (change == ItemVisibleHasChanged || change == ItemSceneChange) { + relayout(); + } + QQuickItem::itemChange(change, data); +} + +void ToolBarLayout::updatePolish() +{ + d->performLayout(); +} + +/** + * Calculate the implicit size for this layout. + * + * This is a separate step from performing the actual layout, because of a nasty + * little issue with Control, where it will unconditionally set the height of + * its contentItem, which means QQuickItem::heightValid() becomes useless. So + * instead, we first calculate our implicit size, ignoring any explicitly set + * item size. Then we follow that by performing the actual layouting, using the + * width and height retrieved from the item, as those will return the explicitly + * set width/height if set and the implicit size otherwise. Since control + * watches for implicit size changes, we end up with correct behaviour both when + * we get an explicit size set and when we're relying on implicit size + * calculation. + */ +void ToolBarLayoutPrivate::calculateImplicitSize() +{ + if (!completed) { + return; + } + + if (!fullDelegate || !iconDelegate || !separatorDelegate || !moreButton) { + qCWarning(KirigamiLayoutsLog) << "ToolBarLayout: Unable to layout, required properties are not set"; + return; + } + + if (actions.isEmpty()) { + q->setImplicitSize(0., 0.); + return; + } + + hiddenActions.clear(); + firstHiddenIndex = -1; + + sortedDelegates = createDelegates(); + + bool ready = std::all_of(delegates.cbegin(), delegates.cend(), [](const std::pair> &entry) { + return entry.second->isReady(); + }); + if (!ready || !moreButtonInstance) { + return; + } + + qreal maxHeight = 0.0; + qreal maxWidth = 0.0; + + // First, calculate the total width and maximum height of all delegates. + // This will be used to determine which actions to show, which ones to + // collapse to icon-only etc. + for (auto entry : std::as_const(sortedDelegates)) { + if (!entry->isActionVisible()) { + entry->hide(); + continue; + } + + if (entry->isHidden()) { + entry->hide(); + hiddenActions.append(entry->action()); + continue; + } + + if (entry->isIconOnly()) { + entry->showIcon(); + } else { + entry->showFull(); + } + + maxWidth += entry->width() + spacing; + maxHeight = std::max(maxHeight, entry->maxHeight()); + } + + // The last entry also gets spacing but shouldn't, so remove that. + maxWidth -= spacing; + + visibleActionsWidth = 0.0; + + if (maxWidth > q->width() - (hiddenActions.isEmpty() ? 0.0 : moreButtonInstance->width() + spacing)) { + // We have more items than fit into the view, so start hiding some. + + qreal layoutWidth = q->width() - (moreButtonInstance->width() + spacing); + if (alignment & Qt::AlignHCenter) { + // When centering, we need to reserve space on both sides to make sure + // things are properly centered, otherwise we will be to the right of + // the center. + layoutWidth -= (moreButtonInstance->width() + spacing); + } + + for (int i = 0; i < sortedDelegates.size(); ++i) { + auto delegate = sortedDelegates.at(i); + + maybeHideDelegate(i, visibleActionsWidth, layoutWidth); + + if (delegate->isVisible()) { + visibleActionsWidth += delegate->width() + spacing; + } + } + if (!qFuzzyIsNull(visibleActionsWidth)) { + // Like above, remove spacing on the last element that incorrectly gets spacing added. + visibleActionsWidth -= spacing; + } + } else { + visibleActionsWidth = maxWidth; + } + + if (!hiddenActions.isEmpty()) { + maxHeight = std::max(maxHeight, moreButtonInstance->implicitHeight()); + }; + + q->setImplicitSize(maxWidth, maxHeight); + Q_EMIT q->hiddenActionsChanged(); + + implicitSizeValid = true; + + q->polish(); +} + +void ToolBarLayoutPrivate::performLayout() +{ + if (!completed || actions.isEmpty()) { + return; + } + + if (!implicitSizeValid) { + calculateImplicitSize(); + } + + if (sortedDelegates.isEmpty()) { + sortedDelegates = createDelegates(); + } + + bool ready = std::all_of(delegates.cbegin(), delegates.cend(), [](const std::pair> &entry) { + return entry.second->isReady(); + }); + if (!ready || !moreButtonInstance) { + return; + } + + qreal width = q->width(); + qreal height = q->height(); + + if (!hiddenActions.isEmpty()) { + if (layoutDirection == Qt::LeftToRight) { + moreButtonInstance->setX(width - moreButtonInstance->width()); + } else { + moreButtonInstance->setX(0.0); + } + + if (heightMode == ToolBarLayout::AlwaysFill) { + moreButtonInstance->setHeight(height); + } else if (heightMode == ToolBarLayout::ConstrainIfLarger) { + if (moreButtonInstance->implicitHeight() > height) { + moreButtonInstance->setHeight(height); + } else { + moreButtonInstance->resetHeight(); + } + } else { + moreButtonInstance->resetHeight(); + } + + moreButtonInstance->setY(qRound((height - moreButtonInstance->height()) / 2.0)); + shouldShowMoreButton = true; + moreButtonInstance->setVisible(true); + } else { + shouldShowMoreButton = false; + moreButtonInstance->setVisible(false); + } + + qreal currentX = layoutStart(visibleActionsWidth); + for (auto entry : std::as_const(sortedDelegates)) { + if (!entry->isVisible()) { + continue; + } + + if (heightMode == ToolBarLayout::AlwaysFill) { + entry->setHeight(height); + } else if (heightMode == ToolBarLayout::ConstrainIfLarger) { + if (entry->implicitHeight() > height) { + entry->setHeight(height); + } else { + entry->resetHeight(); + } + } else { + entry->resetHeight(); + } + + qreal y = qRound((height - entry->height()) / 2.0); + + if (layoutDirection == Qt::LeftToRight) { + entry->setPosition(currentX, y); + currentX += entry->width() + spacing; + } else { + entry->setPosition(currentX - entry->width(), y); + currentX -= entry->width() + spacing; + } + + entry->show(); + } + + qreal newVisibleWidth = visibleActionsWidth; + if (moreButtonInstance->isVisible()) { + newVisibleWidth += moreButtonInstance->width() + (newVisibleWidth > 0.0 ? spacing : 0.0); + } + if (!qFuzzyCompare(newVisibleWidth, visibleWidth)) { + visibleWidth = newVisibleWidth; + Q_EMIT q->visibleWidthChanged(); + } + + if (actionsChanged) { + // Due to the way QQmlListProperty works, if we emit changed every time + // an action is added/removed, we end up emitting way too often. So + // instead only do it after everything else is done. + Q_EMIT q->actionsChanged(); + actionsChanged = false; + } + + sortedDelegates.clear(); +} + +QList ToolBarLayoutPrivate::createDelegates() +{ + QList result; + for (auto action : std::as_const(actions)) { + if (delegates.find(action) != delegates.end()) { + result.append(delegates.at(action).get()); + } else if (action) { + auto delegate = std::unique_ptr(createDelegate(action)); + if (delegate) { + result.append(delegate.get()); + delegates.emplace(action, std::move(delegate)); + } + } + } + + if (!moreButtonInstance && !moreButtonIncubator) { + moreButtonIncubator = new ToolBarDelegateIncubator(moreButton, qmlContext(moreButton)); + moreButtonIncubator->setStateCallback([this](QQuickItem *item) { + item->setParentItem(q); + }); + moreButtonIncubator->setCompletedCallback([this](ToolBarDelegateIncubator *incubator) { + moreButtonInstance = qobject_cast(incubator->object()); + moreButtonInstance->setVisible(false); + + QObject::connect(moreButtonInstance, &QQuickItem::visibleChanged, q, [this]() { + moreButtonInstance->setVisible(shouldShowMoreButton); + }); + QObject::connect(moreButtonInstance, &QQuickItem::widthChanged, q, &ToolBarLayout::minimumWidthChanged); + q->relayout(); + Q_EMIT q->minimumWidthChanged(); + + QTimer::singleShot(0, q, [this]() { + delete moreButtonIncubator; + moreButtonIncubator = nullptr; + }); + }); + moreButtonIncubator->create(); + } + + return result; +} + +ToolBarLayoutDelegate *ToolBarLayoutPrivate::createDelegate(QObject *action) +{ + QQmlComponent *fullComponent = nullptr; + auto displayComponent = action->property("displayComponent"); + if (displayComponent.isValid()) { + fullComponent = displayComponent.value(); + } + + if (!fullComponent) { + fullComponent = fullDelegate; + } + + auto separator = action->property("separator"); + if (separator.isValid() && separator.toBool()) { + fullComponent = separatorDelegate; + } + + auto result = new ToolBarLayoutDelegate(q); + result->setAction(action); + result->createItems(fullComponent, iconDelegate, [this, action](QQuickItem *newItem) { + newItem->setParentItem(q); + auto attached = static_cast(qmlAttachedPropertiesObject(newItem, true)); + attached->setAction(action); + + if (!q->childItems().isEmpty() && q->childItems().first() != newItem) { + // Due to asynchronous item creation, we end up creating the last item + // first. So move items before previously inserted items to ensure + // we have a more sensible tab order. + // Note that this will be incorrect if we end up completing in random + // order. + newItem->stackBefore(q->childItems().first()); + } + }); + + return result; +} + +qreal ToolBarLayoutPrivate::layoutStart(qreal layoutWidth) +{ + qreal availableWidth = moreButtonInstance->isVisible() ? q->width() - (moreButtonInstance->width() + spacing) : q->width(); + + if (alignment & Qt::AlignLeft) { + return layoutDirection == Qt::LeftToRight ? 0.0 : q->width(); + } else if (alignment & Qt::AlignHCenter) { + return (q->width() / 2) + (layoutDirection == Qt::LeftToRight ? -layoutWidth / 2.0 : layoutWidth / 2.0); + } else if (alignment & Qt::AlignRight) { + qreal offset = availableWidth - layoutWidth; + return layoutDirection == Qt::LeftToRight ? offset : q->width() - offset; + } + return 0.0; +} + +void ToolBarLayoutPrivate::maybeHideDelegate(int index, qreal ¤tWidth, qreal totalWidth) +{ + auto delegate = sortedDelegates.at(index); + + if (!delegate->isVisible()) { + // If the delegate isn't visible anyway, do nothing. + return; + } + + if (currentWidth + delegate->width() < totalWidth && (firstHiddenIndex < 0 || index < firstHiddenIndex)) { + // If the delegate is fully visible and we have not already hidden + // actions, do nothing. + return; + } + + if (delegate->isKeepVisible()) { + // If the action is marked as KeepVisible, we need to try our best to + // keep it in view. If the full size delegate does not fit, we try the + // icon-only delegate. If that also does not fit, try and find other + // actions to hide. Finally, if that also fails, we will hide the + // delegate. + if (currentWidth + delegate->iconWidth() > totalWidth) { + // First, hide any earlier actions that are not marked as KeepVisible. + for (auto currentIndex = index - 1; currentIndex >= 0; --currentIndex) { + auto previousDelegate = sortedDelegates.at(currentIndex); + if (!previousDelegate->isVisible() || previousDelegate->isKeepVisible()) { + continue; + } + + auto width = previousDelegate->width(); + previousDelegate->hide(); + hiddenActions.append(previousDelegate->action()); + currentWidth -= (width + spacing); + + if (currentWidth + delegate->fullWidth() <= totalWidth) { + delegate->showFull(); + break; + } else if (currentWidth + delegate->iconWidth() <= totalWidth) { + delegate->showIcon(); + break; + } + } + + if (currentWidth + delegate->width() <= totalWidth) { + return; + } + + // Hiding normal actions did not help enough, so go through actions + // with KeepVisible set and try and collapse them to IconOnly. + for (auto currentIndex = index - 1; currentIndex >= 0; --currentIndex) { + auto previousDelegate = sortedDelegates.at(currentIndex); + if (!previousDelegate->isVisible() || !previousDelegate->isKeepVisible()) { + continue; + } + + auto extraSpace = previousDelegate->width() - previousDelegate->iconWidth(); + previousDelegate->showIcon(); + currentWidth -= extraSpace; + + if (currentWidth + delegate->fullWidth() <= totalWidth) { + delegate->showFull(); + break; + } else if (currentWidth + delegate->iconWidth() <= totalWidth) { + delegate->showIcon(); + break; + } + } + + // If that also did not work, then hide this action after all. + if (currentWidth + delegate->width() > totalWidth) { + delegate->hide(); + hiddenActions.append(delegate->action()); + } + } else { + delegate->showIcon(); + } + } else { + // The action is not marked as KeepVisible and it does not fit within + // the current layout, so hide it. + delegate->hide(); + hiddenActions.append(delegate->action()); + + // If this is the first item to be hidden, mark it so we know we should + // also hide the following items. + if (firstHiddenIndex < 0) { + firstHiddenIndex = index; + } + } +} + +void ToolBarLayoutPrivate::appendAction(ToolBarLayout::ActionsProperty *list, QObject *action) +{ + auto layout = reinterpret_cast(list->data); + layout->addAction(action); +} + +qsizetype ToolBarLayoutPrivate::actionCount(ToolBarLayout::ActionsProperty *list) +{ + return reinterpret_cast(list->data)->d->actions.count(); +} + +QObject *ToolBarLayoutPrivate::action(ToolBarLayout::ActionsProperty *list, qsizetype index) +{ + return reinterpret_cast(list->data)->d->actions.at(index); +} + +void ToolBarLayoutPrivate::clearActions(ToolBarLayout::ActionsProperty *list) +{ + reinterpret_cast(list->data)->clearActions(); +} + +#include "moc_toolbarlayout.cpp" diff --git a/src/layouts/toolbarlayout.h b/src/layouts/toolbarlayout.h new file mode 100644 index 0000000..9f17497 --- /dev/null +++ b/src/layouts/toolbarlayout.h @@ -0,0 +1,245 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +#ifndef TOOLBARLAYOUT_H +#define TOOLBARLAYOUT_H + +#include +#include + +class ToolBarLayoutPrivate; + +/** + * Attached property for ToolBarLayout delegates. + */ +class ToolBarLayoutAttached : public QObject +{ + Q_OBJECT + /** + * The action this delegate was created for. + */ + Q_PROPERTY(QObject *action READ action CONSTANT FINAL) +public: + ToolBarLayoutAttached(QObject *parent = nullptr); + + QObject *action() const; + void setAction(QObject *action); + +private: + QObject *m_action = nullptr; +}; + +/** + * An item that creates delegates for actions and lays them out in a row. + * + * This effectively combines RowLayout and Repeater in a single item, with the + * addition of some extra performance enhancing tweaks. It will create instances + * of ::fullDelegate and ::itemDelegate for each action in ::actions . These are + * then positioned horizontally. Any action that ends up being placed outside + * the width of the item is hidden and will be part of ::hiddenActions. + * + * The items created as delegates are always created asynchronously, to avoid + * creation lag spikes. Each delegate has access to the action it was created + * for through the ToolBarLayoutAttached attached property. + */ +class ToolBarLayout : public QQuickItem +{ + Q_OBJECT + QML_ELEMENT + QML_ATTACHED(ToolBarLayoutAttached) + /** + * The actions this layout should create delegates for. + */ + Q_PROPERTY(QQmlListProperty actions READ actionsProperty NOTIFY actionsChanged FINAL) + /** + * A list of actions that do not fit in the current view and are thus hidden. + */ + Q_PROPERTY(QList hiddenActions READ hiddenActions NOTIFY hiddenActionsChanged FINAL) + /** + * A component that is used to create full-size delegates from. + * + * Each delegate has three states, it can be full-size, icon-only or hidden. + * By default, the full-size delegate is used. When the action has the + * DisplayHint::IconOnly hint set, it will always use the iconDelegate. When + * it has the DisplayHint::KeepVisible hint set, it will use the full-size + * delegate when it fits. If not, it will use the iconDelegate, unless even + * that does not fit, in which case it will still be hidden. + */ + Q_PROPERTY(QQmlComponent *fullDelegate READ fullDelegate WRITE setFullDelegate NOTIFY fullDelegateChanged FINAL) + /** + * A component that is used to create icon-only delegates from. + * + * \sa fullDelegate + */ + Q_PROPERTY(QQmlComponent *iconDelegate READ iconDelegate WRITE setIconDelegate NOTIFY iconDelegateChanged FINAL) + /** + * A component that is used to create separator delegates from. + * + * \since 6.7 + * + * \sa fullDelegate + */ + Q_PROPERTY(QQmlComponent *separatorDelegate READ separatorDelegate WRITE setSeparatorDelegate NOTIFY separatorDelegateChanged FINAL) + /** + * A component that is used to create the "more button" item from. + * + * The more button is shown when there are actions that do not fit the + * current view. It is intended to have functionality to show these hidden + * actions, like popup a menu with them showing. + */ + Q_PROPERTY(QQmlComponent *moreButton READ moreButton WRITE setMoreButton NOTIFY moreButtonChanged FINAL) + /** + * The amount of spacing between individual delegates. + */ + Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged FINAL) + /** + * How to align the delegates within this layout. + * + * When there is more space available than required by the visible delegates, + * we need to determine how to place the delegates. This property determines + * how to do that. Note that the moreButton, if visible, will always be + * placed at the end of the layout. + */ + Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged FINAL) + /** + * The combined width of visible delegates in this layout. + */ + Q_PROPERTY(qreal visibleWidth READ visibleWidth NOTIFY visibleWidthChanged FINAL) + /** + * The minimum width this layout can have. + * + * This is equal to the width of the moreButton. + */ + Q_PROPERTY(qreal minimumWidth READ minimumWidth NOTIFY minimumWidthChanged FINAL) + /** + * Which direction to layout in. + * + * This is primarily intended to support right-to-left layouts. When set to + * LeftToRight, delegates will be layout with the first item on the left and + * following items to the right of that. The more button will be placed at + * the rightmost position. Alignment flags work normally. + * + * When set to RightToLeft, delegates will be layout with the first item on + * the right and following items to the left of that. The more button will + * be placed at the leftmost position. Alignment flags are inverted, so + * AlignLeft will align items to the right, and vice-versa. + */ + Q_PROPERTY(Qt::LayoutDirection layoutDirection READ layoutDirection WRITE setLayoutDirection NOTIFY layoutDirectionChanged FINAL) + /** + * How to handle items that do not match the toolbar's height. + * + * When toolbar items do not match the height of the toolbar, there are + * several ways we can deal with this. This property sets the preferred way. + * + * The default is HeightMode::ConstrainIfLarger . + * + * \sa HeightMode + */ + Q_PROPERTY(HeightMode heightMode READ heightMode WRITE setHeightMode NOTIFY heightModeChanged FINAL) + +public: + using ActionsProperty = QQmlListProperty; + + /** + * An enum describing several modes that can be used to deal with items with + * a height that does not match the toolbar's height. + */ + enum HeightMode { + AlwaysCenter, ///< Always center items, allowing them to go outside the bounds of the layout if they are larger. + AlwaysFill, ///< Always match the height of the layout. Larger items will be reduced in height, smaller items will be increased. + ConstrainIfLarger, ///< If the item is larger than the toolbar, reduce its height. Otherwise center it in the toolbar. + }; + Q_ENUM(HeightMode) + + ToolBarLayout(QQuickItem *parent = nullptr); + ~ToolBarLayout() override; + + ActionsProperty actionsProperty() const; + /** + * Add an action to the list of actions. + * + * \param action The action to add. + */ + void addAction(QObject *action); + /** + * Remove an action from the list of actions. + * + * \param action The action to remove. + */ + void removeAction(QObject *action); + /** + * Clear the list of actions. + */ + void clearActions(); + Q_SIGNAL void actionsChanged(); + + QList hiddenActions() const; + Q_SIGNAL void hiddenActionsChanged(); + + QQmlComponent *fullDelegate() const; + void setFullDelegate(QQmlComponent *newFullDelegate); + Q_SIGNAL void fullDelegateChanged(); + + QQmlComponent *iconDelegate() const; + void setIconDelegate(QQmlComponent *newIconDelegate); + Q_SIGNAL void iconDelegateChanged(); + + QQmlComponent *separatorDelegate() const; + void setSeparatorDelegate(QQmlComponent *newSeparatorDelegate); + Q_SIGNAL void separatorDelegateChanged(); + + QQmlComponent *moreButton() const; + void setMoreButton(QQmlComponent *newMoreButton); + Q_SIGNAL void moreButtonChanged(); + + qreal spacing() const; + void setSpacing(qreal newSpacing); + Q_SIGNAL void spacingChanged(); + + Qt::Alignment alignment() const; + void setAlignment(Qt::Alignment newAlignment); + Q_SIGNAL void alignmentChanged(); + + qreal visibleWidth() const; + Q_SIGNAL void visibleWidthChanged(); + + qreal minimumWidth() const; + Q_SIGNAL void minimumWidthChanged(); + + Qt::LayoutDirection layoutDirection() const; + void setLayoutDirection(Qt::LayoutDirection &newLayoutDirection); + Q_SIGNAL void layoutDirectionChanged(); + + HeightMode heightMode() const; + void setHeightMode(HeightMode newHeightMode); + Q_SIGNAL void heightModeChanged(); + + /** + * Queue a relayout of this layout. + * + * \note The layouting happens during the next scene graph polishing phase. + */ + Q_SLOT void relayout(); + + static ToolBarLayoutAttached *qmlAttachedProperties(QObject *object) + { + return new ToolBarLayoutAttached(object); + } + +protected: + void componentComplete() override; + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) override; + void updatePolish() override; + +private: + friend class ToolBarLayoutPrivate; + const std::unique_ptr d; +}; + +QML_DECLARE_TYPEINFO(ToolBarLayout, QML_HAS_ATTACHED_PROPERTIES) + +#endif // TOOLBARLAYOUT_H diff --git a/src/layouts/toolbarlayoutdelegate.cpp b/src/layouts/toolbarlayoutdelegate.cpp new file mode 100644 index 0000000..4da7f72 --- /dev/null +++ b/src/layouts/toolbarlayoutdelegate.cpp @@ -0,0 +1,330 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +#include "toolbarlayoutdelegate.h" + +#include "loggingcategory.h" +#include "toolbarlayout.h" + +ToolBarDelegateIncubator::ToolBarDelegateIncubator(QQmlComponent *component, QQmlContext *context) + : QQmlIncubator(QQmlIncubator::Asynchronous) + , m_component(component) + , m_context(context) +{ +} + +void ToolBarDelegateIncubator::setStateCallback(std::function callback) +{ + m_stateCallback = callback; +} + +void ToolBarDelegateIncubator::setCompletedCallback(std::function callback) +{ + m_completedCallback = callback; +} + +void ToolBarDelegateIncubator::create() +{ + m_component->create(*this, m_context); +} + +bool ToolBarDelegateIncubator::isFinished() +{ + return m_finished; +} + +void ToolBarDelegateIncubator::setInitialState(QObject *object) +{ + auto item = qobject_cast(object); + if (item) { + m_stateCallback(item); + } +} + +void ToolBarDelegateIncubator::statusChanged(QQmlIncubator::Status status) +{ + if (status == QQmlIncubator::Error) { + qCWarning(KirigamiLayoutsLog) << "Could not create delegate for ToolBarLayout"; + const auto e = errors(); + for (const auto &error : e) { + qCWarning(KirigamiLayoutsLog) << error; + } + m_finished = true; + } + + if (status == QQmlIncubator::Ready) { + m_completedCallback(this); + m_finished = true; + } +} + +ToolBarLayoutDelegate::ToolBarLayoutDelegate(ToolBarLayout *parent) + : QObject() // Note: delegates are managed by unique_ptr, so don't parent + , m_parent(parent) +{ +} + +ToolBarLayoutDelegate::~ToolBarLayoutDelegate() +{ + if (m_fullIncubator) { + m_fullIncubator->clear(); + delete m_fullIncubator; + } + if (m_iconIncubator) { + m_iconIncubator->clear(); + delete m_iconIncubator; + } + if (m_full) { + m_full->disconnect(this); + delete m_full; + } + if (m_icon) { + m_icon->disconnect(this); + delete m_icon; + } +} + +QObject *ToolBarLayoutDelegate::action() const +{ + return m_action; +} + +void ToolBarLayoutDelegate::setAction(QObject *action) +{ + if (action == m_action) { + return; + } + + if (m_action) { + QObject::disconnect(m_action, SIGNAL(visibleChanged()), this, SLOT(actionVisibleChanged())); + QObject::disconnect(m_action, SIGNAL(displayHintChanged()), this, SLOT(displayHintChanged())); + } + + m_action = action; + if (m_action) { + if (m_action->property("visible").isValid()) { + QObject::connect(m_action, SIGNAL(visibleChanged()), this, SLOT(actionVisibleChanged())); + m_actionVisible = m_action->property("visible").toBool(); + } + + if (m_action->property("displayHint").isValid()) { + QObject::connect(m_action, SIGNAL(displayHintChanged()), this, SLOT(displayHintChanged())); + m_displayHint = DisplayHint::DisplayHints{m_action->property("displayHint").toInt()}; + } + } +} + +void ToolBarLayoutDelegate::createItems(QQmlComponent *fullComponent, QQmlComponent *iconComponent, std::function callback) +{ + m_fullIncubator = new ToolBarDelegateIncubator(fullComponent, qmlContext(fullComponent)); + m_fullIncubator->setStateCallback(callback); + m_fullIncubator->setCompletedCallback([this](ToolBarDelegateIncubator *incubator) { + if (incubator->isError()) { + qCWarning(KirigamiLayoutsLog) << "Could not create delegate for ToolBarLayout"; + const auto errors = incubator->errors(); + for (const auto &error : errors) { + qCWarning(KirigamiLayoutsLog) << error; + } + return; + } + + m_full = qobject_cast(incubator->object()); + m_full->setVisible(false); + connect(m_full, &QQuickItem::implicitWidthChanged, this, &ToolBarLayoutDelegate::triggerRelayout); + connect(m_full, &QQuickItem::implicitHeightChanged, this, &ToolBarLayoutDelegate::triggerRelayout); + connect(m_full, &QQuickItem::visibleChanged, this, &ToolBarLayoutDelegate::ensureItemVisibility); + + if (m_icon) { + m_ready = true; + } + + m_parent->relayout(); + + QMetaObject::invokeMethod(this, &ToolBarLayoutDelegate::cleanupIncubators, Qt::QueuedConnection); + }); + m_iconIncubator = new ToolBarDelegateIncubator(iconComponent, qmlContext(iconComponent)); + m_iconIncubator->setStateCallback(callback); + m_iconIncubator->setCompletedCallback([this](ToolBarDelegateIncubator *incubator) { + if (incubator->isError()) { + qCWarning(KirigamiLayoutsLog) << "Could not create delegate for ToolBarLayout"; + const auto errors = incubator->errors(); + for (const auto &error : errors) { + qCWarning(KirigamiLayoutsLog) << error; + } + return; + } + + m_icon = qobject_cast(incubator->object()); + m_icon->setVisible(false); + connect(m_icon, &QQuickItem::implicitWidthChanged, this, &ToolBarLayoutDelegate::triggerRelayout); + connect(m_icon, &QQuickItem::implicitHeightChanged, this, &ToolBarLayoutDelegate::triggerRelayout); + connect(m_icon, &QQuickItem::visibleChanged, this, &ToolBarLayoutDelegate::ensureItemVisibility); + + if (m_full) { + m_ready = true; + } + + m_parent->relayout(); + + QMetaObject::invokeMethod(this, &ToolBarLayoutDelegate::cleanupIncubators, Qt::QueuedConnection); + }); + + m_fullIncubator->create(); + m_iconIncubator->create(); +} + +bool ToolBarLayoutDelegate::isReady() const +{ + return m_ready; +} + +bool ToolBarLayoutDelegate::isActionVisible() const +{ + return m_actionVisible; +} + +bool ToolBarLayoutDelegate::isHidden() const +{ + return DisplayHint::isDisplayHintSet(m_displayHint, DisplayHint::AlwaysHide); +} + +bool ToolBarLayoutDelegate::isIconOnly() const +{ + return DisplayHint::isDisplayHintSet(m_displayHint, DisplayHint::IconOnly); +} + +bool ToolBarLayoutDelegate::isKeepVisible() const +{ + return DisplayHint::isDisplayHintSet(m_displayHint, DisplayHint::KeepVisible); +} + +bool ToolBarLayoutDelegate::isVisible() const +{ + return m_iconVisible || m_fullVisible; +} + +void ToolBarLayoutDelegate::hide() +{ + m_iconVisible = false; + m_fullVisible = false; + ensureItemVisibility(); +} + +void ToolBarLayoutDelegate::showFull() +{ + m_iconVisible = false; + m_fullVisible = true; +} + +void ToolBarLayoutDelegate::showIcon() +{ + m_iconVisible = true; + m_fullVisible = false; +} + +void ToolBarLayoutDelegate::show() +{ + ensureItemVisibility(); +} + +void ToolBarLayoutDelegate::setPosition(qreal x, qreal y) +{ + m_full->setX(x); + m_icon->setX(x); + m_full->setY(y); + m_icon->setY(y); +} + +void ToolBarLayoutDelegate::setHeight(qreal height) +{ + m_full->setHeight(height); + m_icon->setHeight(height); +} + +void ToolBarLayoutDelegate::resetHeight() +{ + m_full->resetHeight(); + m_icon->resetHeight(); +} + +qreal ToolBarLayoutDelegate::width() const +{ + if (m_iconVisible) { + return m_icon->width(); + } + return m_full->width(); +} + +qreal ToolBarLayoutDelegate::height() const +{ + if (m_iconVisible) { + return m_icon->height(); + } + return m_full->height(); +} + +qreal ToolBarLayoutDelegate::implicitWidth() const +{ + if (m_iconVisible) { + return m_icon->implicitWidth(); + } + return m_full->implicitWidth(); +} + +qreal ToolBarLayoutDelegate::implicitHeight() const +{ + if (m_iconVisible) { + return m_icon->implicitHeight(); + } + return m_full->implicitHeight(); +} + +qreal ToolBarLayoutDelegate::maxHeight() const +{ + return std::max(m_full->implicitHeight(), m_icon->implicitHeight()); +} + +qreal ToolBarLayoutDelegate::iconWidth() const +{ + return m_icon->width(); +} + +qreal ToolBarLayoutDelegate::fullWidth() const +{ + return m_full->width(); +} + +void ToolBarLayoutDelegate::actionVisibleChanged() +{ + m_actionVisible = m_action->property("visible").toBool(); + m_parent->relayout(); +} + +void ToolBarLayoutDelegate::displayHintChanged() +{ + m_displayHint = DisplayHint::DisplayHints{m_action->property("displayHint").toInt()}; + m_parent->relayout(); +} + +void ToolBarLayoutDelegate::cleanupIncubators() +{ + if (m_fullIncubator && m_fullIncubator->isFinished()) { + delete m_fullIncubator; + m_fullIncubator = nullptr; + } + + if (m_iconIncubator && m_iconIncubator->isFinished()) { + delete m_iconIncubator; + m_iconIncubator = nullptr; + } +} + +void ToolBarLayoutDelegate::triggerRelayout() +{ + m_parent->relayout(); +} + +#include "moc_toolbarlayoutdelegate.cpp" diff --git a/src/layouts/toolbarlayoutdelegate.h b/src/layouts/toolbarlayoutdelegate.h new file mode 100644 index 0000000..81568bd --- /dev/null +++ b/src/layouts/toolbarlayoutdelegate.h @@ -0,0 +1,113 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +#ifndef TOOLBARLAYOUTDELEGATE_H +#define TOOLBARLAYOUTDELEGATE_H + +#include "displayhint.h" +#include +#include + +class ToolBarLayout; + +/* + * A helper subclass of QQmlIncubator that allows us to do some things more + * easily. + */ +class ToolBarDelegateIncubator : public QQmlIncubator +{ +public: + ToolBarDelegateIncubator(QQmlComponent *component, QQmlContext *context); + + void setStateCallback(std::function callback); + void setCompletedCallback(std::function callback); + + void create(); + + bool isFinished(); + +private: + void setInitialState(QObject *object) override; + void statusChanged(QQmlIncubator::Status status) override; + + QQmlComponent *m_component; + QQmlContext *m_context; + std::function m_stateCallback; + std::function m_completedCallback; + bool m_finished = false; +}; + +/* + * A helper class to encapsulate some of the delegate functionality used by + * ToolBarLayout. Primarily, this hides some of the difference that delegates + * are two items instead of one. + */ +class ToolBarLayoutDelegate : public QObject +{ + Q_OBJECT +public: + ToolBarLayoutDelegate(ToolBarLayout *parent); + ~ToolBarLayoutDelegate() override; + + QObject *action() const; + void setAction(QObject *action); + void createItems(QQmlComponent *fullComponent, QQmlComponent *iconComponent, std::function callback); + + bool isReady() const; + bool isActionVisible() const; + bool isHidden() const; + bool isIconOnly() const; + bool isKeepVisible() const; + + bool isVisible() const; + + void hide(); + void showIcon(); + void showFull(); + void show(); + + void setPosition(qreal x, qreal y); + void setHeight(qreal height); + void resetHeight(); + + qreal width() const; + qreal height() const; + qreal implicitWidth() const; + qreal implicitHeight() const; + qreal maxHeight() const; + qreal iconWidth() const; + qreal fullWidth() const; + +private: + Q_SLOT void actionVisibleChanged(); + Q_SLOT void displayHintChanged(); + inline void ensureItemVisibility() + { + if (m_full) { + m_full->setVisible(m_fullVisible); + } + if (m_icon) { + m_icon->setVisible(m_iconVisible); + } + } + void cleanupIncubators(); + void triggerRelayout(); + + ToolBarLayout *m_parent = nullptr; + QObject *m_action = nullptr; + QQuickItem *m_full = nullptr; + QQuickItem *m_icon = nullptr; + ToolBarDelegateIncubator *m_fullIncubator = nullptr; + ToolBarDelegateIncubator *m_iconIncubator = nullptr; + + DisplayHint::DisplayHints m_displayHint = DisplayHint::NoPreference; + bool m_ready = false; + bool m_actionVisible = true; + bool m_fullVisible = false; + bool m_iconVisible = false; +}; + +#endif // TOOLBARLAYOUTDELEGATE_H diff --git a/src/mnemonicattached.cpp b/src/mnemonicattached.cpp new file mode 100644 index 0000000..848a487 --- /dev/null +++ b/src/mnemonicattached.cpp @@ -0,0 +1,504 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "mnemonicattached.h" +#include +#include +#include +#include +#include +#include +#include + +QHash MnemonicAttached::s_sequenceToObject = QHash(); + +// If pos points to alphanumeric X in "...(X)...", which is preceded or +// followed only by non-alphanumerics, then "(X)" gets removed. +static QString removeReducedCJKAccMark(const QString &label, int pos) +{ + /* clang-format off */ + if (pos > 0 && pos + 1 < label.length() + && label[pos - 1] == QLatin1Char('(') + && label[pos + 1] == QLatin1Char(')') + && label[pos].isLetterOrNumber()) { /* clang-format on */ + // Check if at start or end, ignoring non-alphanumerics. + int len = label.length(); + int p1 = pos - 2; + while (p1 >= 0 && !label[p1].isLetterOrNumber()) { + --p1; + } + ++p1; + int p2 = pos + 2; + while (p2 < len && !label[p2].isLetterOrNumber()) { + ++p2; + } + --p2; + + const QStringView strView(label); + if (p1 == 0) { + return strView.left(pos - 1) + strView.mid(p2 + 1); + } else if (p2 + 1 == len) { + return strView.left(p1) + strView.mid(pos + 2); + } + } + return label; +} + +static QString removeAcceleratorMarker(const QString &label_) +{ + QString label = label_; + + int p = 0; + bool accmarkRemoved = false; + while (true) { + p = label.indexOf(QLatin1Char('&'), p); + if (p < 0 || p + 1 == label.length()) { + break; + } + + if (label.at(p + 1).isLetterOrNumber()) { + // Valid accelerator. + const QStringView sv(label); + label = sv.left(p) + sv.mid(p + 1); + + // May have been an accelerator in CJK-style "(&X)" + // at the start or end of text. + label = removeReducedCJKAccMark(label, p); + + accmarkRemoved = true; + } else if (label.at(p + 1) == QLatin1Char('&')) { + // Escaped accelerator marker. + const QStringView sv(label); + label = sv.left(p) + sv.mid(p + 1); + } + + ++p; + } + + // If no marker was removed, and there are CJK characters in the label, + // also try to remove reduced CJK marker -- something may have removed + // ampersand beforehand. + if (!accmarkRemoved) { + bool hasCJK = false; + for (const QChar c : std::as_const(label)) { + if (c.unicode() >= 0x2e00) { // rough, but should be sufficient + hasCJK = true; + break; + } + } + if (hasCJK) { + p = 0; + while (true) { + p = label.indexOf(QLatin1Char('('), p); + if (p < 0) { + break; + } + label = removeReducedCJKAccMark(label, p + 1); + ++p; + } + } + } + + return label; +} + +class MnemonicEventFilter : public QObject +{ + Q_OBJECT + +public: + static MnemonicEventFilter &instance() + { + static MnemonicEventFilter s_instance; + return s_instance; + } + + bool eventFilter(QObject *watched, QEvent *event) override + { + Q_UNUSED(watched); + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *ke = static_cast(event); + if (ke->key() == Qt::Key_Alt) { + m_altPressed = true; + Q_EMIT altPressed(); + } + } else if (event->type() == QEvent::KeyRelease) { + QKeyEvent *ke = static_cast(event); + if (ke->key() == Qt::Key_Alt) { + m_altPressed = false; + Q_EMIT altReleased(); + } + } else if (event->type() == QEvent::ApplicationStateChange) { + if (m_altPressed) { + m_altPressed = false; + Q_EMIT altReleased(); + } + } + + return false; + } + +Q_SIGNALS: + void altPressed(); + void altReleased(); + +private: + MnemonicEventFilter() + : QObject(nullptr) + { + qGuiApp->installEventFilter(this); + } + + bool m_altPressed = false; +}; + +MnemonicAttached::MnemonicAttached(QObject *parent) + : QObject(parent) +{ + connect(&MnemonicEventFilter::instance(), &MnemonicEventFilter::altPressed, this, &MnemonicAttached::onAltPressed); + connect(&MnemonicEventFilter::instance(), &MnemonicEventFilter::altReleased, this, &MnemonicAttached::onAltReleased); +} + +MnemonicAttached::~MnemonicAttached() +{ + s_sequenceToObject.remove(m_sequence); +} + +QWindow *MnemonicAttached::window() const +{ + if (auto *parentItem = qobject_cast(parent())) { + if (auto *window = parentItem->window()) { + if (auto *renderWindow = QQuickRenderControl::renderWindowFor(window)) { + return renderWindow; + } + + return window; + } + } + + return nullptr; +} + +void MnemonicAttached::onAltPressed() +{ + if (m_active || !m_enabled || m_richTextLabel.isEmpty()) { + return; + } + + auto *win = window(); + if (!win || !win->isActive()) { + return; + } + + m_actualRichTextLabel = m_richTextLabel; + Q_EMIT richTextLabelChanged(); + m_active = true; + Q_EMIT activeChanged(); +} + +void MnemonicAttached::onAltReleased() +{ + if (!m_active || m_richTextLabel.isEmpty()) { + return; + } + + // Disabling menmonics again is always fine, e.g. on window deactivation, + // don't check for enabled or window is active here. + + m_actualRichTextLabel = removeAcceleratorMarker(m_label); + Q_EMIT richTextLabelChanged(); + m_active = false; + Q_EMIT activeChanged(); +} + +// Algorithm adapted from KAccelString +void MnemonicAttached::calculateWeights() +{ + m_weights.clear(); + + int pos = 0; + bool start_character = true; + bool wanted_character = false; + + while (pos < m_label.length()) { + QChar c = m_label[pos]; + + // skip non typeable characters + if (!c.isLetterOrNumber() && c != QLatin1Char('&')) { + start_character = true; + ++pos; + continue; + } + + int weight = 1; + + // add special weight to first character + if (pos == 0) { + weight += FIRST_CHARACTER_EXTRA_WEIGHT; + // add weight to word beginnings + } else if (start_character) { + weight += WORD_BEGINNING_EXTRA_WEIGHT; + start_character = false; + } + + // add weight to characters that have an & beforehand + if (wanted_character) { + weight += WANTED_ACCEL_EXTRA_WEIGHT; + wanted_character = false; + } + + // add decreasing weight to left characters + if (pos < 50) { + weight += (50 - pos); + } + + // try to preserve the wanted accelerators + /* clang-format off */ + if (c == QLatin1Char('&') + && (pos != m_label.length() - 1 + && m_label[pos + 1] != QLatin1Char('&') + && m_label[pos + 1].isLetterOrNumber())) { /* clang-format on */ + wanted_character = true; + ++pos; + continue; + } + + while (m_weights.contains(weight)) { + ++weight; + } + + if (c != QLatin1Char('&')) { + m_weights[weight] = c; + } + + ++pos; + } + + // update our maximum weight + if (m_weights.isEmpty()) { + m_weight = m_baseWeight; + } else { + m_weight = m_baseWeight + (std::prev(m_weights.cend())).key(); + } +} + +void MnemonicAttached::updateSequence() +{ + const QKeySequence oldSequence = m_sequence; + + if (!m_sequence.isEmpty()) { + s_sequenceToObject.remove(m_sequence); + m_sequence = {}; + } + + calculateWeights(); + + // Preserve strings like "One & Two" where & is not an accelerator escape + const QString text = label().replace(QStringLiteral("& "), QStringLiteral("&& ")); + m_actualRichTextLabel = removeAcceleratorMarker(text); + + if (!m_enabled) { + // was the label already completely plain text? try to limit signal emission + if (m_mnemonicLabel != m_actualRichTextLabel) { + m_mnemonicLabel = m_actualRichTextLabel; + Q_EMIT mnemonicLabelChanged(); + Q_EMIT richTextLabelChanged(); + } + + if (m_sequence != oldSequence) { + Q_EMIT sequenceChanged(); + } + return; + } + + m_mnemonicLabel = text; + m_mnemonicLabel.replace(QRegularExpression(QLatin1String("\\&([^\\&])")), QStringLiteral("\\1")); + + if (!m_weights.isEmpty()) { + QMap::const_iterator i = m_weights.constEnd(); + do { + --i; + QChar c = i.value(); + + QKeySequence ks(QStringLiteral("Alt+") % c); + MnemonicAttached *otherMa = s_sequenceToObject.value(ks); + Q_ASSERT(otherMa != this); + if (!otherMa || otherMa->m_weight < m_weight) { + // the old shortcut is less valuable than the current: remove it + if (otherMa) { + s_sequenceToObject.remove(otherMa->sequence()); + otherMa->m_sequence = {}; + } + + s_sequenceToObject[ks] = this; + m_sequence = ks; + m_richTextLabel = text; + m_richTextLabel.replace(QRegularExpression(QLatin1String("\\&([^\\&])")), QStringLiteral("\\1")); + m_mnemonicLabel = text; + const int mnemonicPos = m_mnemonicLabel.indexOf(c); + + if (mnemonicPos > -1 && (mnemonicPos == 0 || m_mnemonicLabel[mnemonicPos - 1] != QLatin1Char('&'))) { + m_mnemonicLabel.replace(mnemonicPos, 1, QStringLiteral("&") % c); + } + + const int richTextPos = m_richTextLabel.indexOf(c); + if (richTextPos > -1) { + m_richTextLabel.replace(richTextPos, 1, QLatin1String("") % c % QLatin1String("")); + } + + // remap the sequence of the previous shortcut + if (otherMa) { + otherMa->updateSequence(); + } + + break; + } + } while (i != m_weights.constBegin()); + } + + if (m_sequence != oldSequence) { + Q_EMIT sequenceChanged(); + } + + Q_EMIT richTextLabelChanged(); + Q_EMIT mnemonicLabelChanged(); +} + +void MnemonicAttached::setLabel(const QString &text) +{ + if (m_label == text) { + return; + } + + m_label = text; + updateSequence(); + Q_EMIT labelChanged(); + Q_EMIT plainTextLabelChanged(); +} + +QString MnemonicAttached::richTextLabel() const +{ + if (!m_actualRichTextLabel.isEmpty()) { + return m_actualRichTextLabel; + } else { + return removeAcceleratorMarker(m_label); + } +} + +QString MnemonicAttached::mnemonicLabel() const +{ + return m_mnemonicLabel; +} + +QString MnemonicAttached::plainTextLabel() const +{ + return removeAcceleratorMarker(m_label); +} + +QString MnemonicAttached::label() const +{ + return m_label; +} + +void MnemonicAttached::setEnabled(bool enabled) +{ + if (m_enabled == enabled) { + return; + } + + m_enabled = enabled; + updateSequence(); + Q_EMIT enabledChanged(); +} + +bool MnemonicAttached::enabled() const +{ + return m_enabled; +} + +void MnemonicAttached::setControlType(MnemonicAttached::ControlType controlType) +{ + if (m_controlType == controlType) { + return; + } + + m_controlType = controlType; + + switch (controlType) { + case ActionElement: + m_baseWeight = ACTION_ELEMENT_WEIGHT; + break; + case DialogButton: + m_baseWeight = DIALOG_BUTTON_EXTRA_WEIGHT; + break; + case MenuItem: + m_baseWeight = MENU_ITEM_WEIGHT; + break; + case FormLabel: + m_baseWeight = FORM_LABEL_WEIGHT; + break; + default: + m_baseWeight = SECONDARY_CONTROL_WEIGHT; + break; + } + // update our maximum weight + if (m_weights.isEmpty()) { + m_weight = m_baseWeight; + } else { + m_weight = m_baseWeight + (std::prev(m_weights.constEnd())).key(); + } + Q_EMIT controlTypeChanged(); +} + +MnemonicAttached::ControlType MnemonicAttached::controlType() const +{ + return m_controlType; +} + +QKeySequence MnemonicAttached::sequence() +{ + return m_sequence; +} + +bool MnemonicAttached::active() const +{ + return m_active; +} + +MnemonicAttached *MnemonicAttached::qmlAttachedProperties(QObject *object) +{ + return new MnemonicAttached(object); +} + +void MnemonicAttached::setActive(bool active) +{ + // We can't rely on previous value when it's true since it can be + // caused by Alt key press and we need to remove the event filter + // additionally. False should be ok as it's a default state. + if (!m_active && m_active == active) { + return; + } + + m_active = active; + + if (m_active) { + if (m_actualRichTextLabel != m_richTextLabel) { + m_actualRichTextLabel = m_richTextLabel; + Q_EMIT richTextLabelChanged(); + } + + } else { + m_actualRichTextLabel = removeAcceleratorMarker(m_label); + Q_EMIT richTextLabelChanged(); + } + + Q_EMIT activeChanged(); +} + +#include "mnemonicattached.moc" + +#include "moc_mnemonicattached.cpp" diff --git a/src/mnemonicattached.h b/src/mnemonicattached.h new file mode 100644 index 0000000..79c7b51 --- /dev/null +++ b/src/mnemonicattached.h @@ -0,0 +1,182 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef MNEMONICATTACHED_H +#define MNEMONICATTACHED_H + +#include +#include + +#include + +/** + * This Attached property is used to calculate automated keyboard sequences + * to trigger actions based upon their text: if an "&" mnemonic is + * used (ie "&Ok"), the system will attempt to assign the desired letter giving + * it priority, otherwise a letter among the ones in the label will be used if + * possible and not conflicting. + * Different kinds of controls will have different priorities in assigning the + * shortcut: for instance the "Ok/Cancel" buttons in a dialog will have priority + * over fields of a FormLayout. + * @see ControlType + * + * Usually the developer shouldn't use this directly as base components + * already use this, but only when implementing a custom graphical Control. + * @since 2.3 + */ +class MnemonicAttached : public QObject +{ + Q_OBJECT + QML_NAMED_ELEMENT(MnemonicData) + QML_ATTACHED(MnemonicAttached) + QML_UNCREATABLE("Cannot create objects of type MnemonicData, use it as an attached property") + /** + * The label of the control we want to compute a mnemonic for, instance + * "Label:" or "&Ok" + */ + Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged FINAL) + + /** + * The user-visible final label, which will have the shortcut letter underlined, + * such as "<u>O</u>k" + */ + Q_PROPERTY(QString richTextLabel READ richTextLabel NOTIFY richTextLabelChanged FINAL) + + /** + * The label with an "&" mnemonic in the place which will have the shortcut + * assigned, regardless of whether the & was assigned by the user or automatically generated. + */ + Q_PROPERTY(QString mnemonicLabel READ mnemonicLabel NOTIFY mnemonicLabelChanged FINAL) + + /** + * The label in plain text with no markup nor & markers. + */ + Q_PROPERTY(QString plainTextLabel READ plainTextLabel NOTIFY plainTextLabelChanged FINAL) + + /** + * Only if true this mnemonic will be considered for the global assignment + * default: true + */ + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged FINAL) + + /** + * The type of control this mnemonic is attached: different types of controls have different importance and priority for shortcut assignment. + * @see ControlType + */ + Q_PROPERTY(MnemonicAttached::ControlType controlType READ controlType WRITE setControlType NOTIFY controlTypeChanged FINAL) + + /** + * The final key sequence assigned, if any: it will be Alt+alphanumeric char + */ + Q_PROPERTY(QKeySequence sequence READ sequence NOTIFY sequenceChanged FINAL) + + /** + * True when the user is pressing alt and the accelerators should be shown + * + * @since 5.72 + * @since 2.15 + */ + Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged FINAL) + +public: + enum ControlType { + ActionElement, /**< pushbuttons, checkboxes etc */ + DialogButton, /**< buttons for dialogs */ + MenuItem, /**< Menu items */ + FormLabel, /**< Buddy label in a FormLayout*/ + SecondaryControl, /**< Other controls that are considered not much important and low priority for shortcuts */ + }; + Q_ENUM(ControlType) + + explicit MnemonicAttached(QObject *parent = nullptr); + ~MnemonicAttached() override; + + void setLabel(const QString &text); + QString label() const; + + QString richTextLabel() const; + QString mnemonicLabel() const; + QString plainTextLabel() const; + + void setEnabled(bool enabled); + bool enabled() const; + + void setControlType(MnemonicAttached::ControlType controlType); + ControlType controlType() const; + + QKeySequence sequence(); + + void setActive(bool active); + bool active() const; + + // QML attached property + static MnemonicAttached *qmlAttachedProperties(QObject *object); + +protected: + void updateSequence(); + +Q_SIGNALS: + void labelChanged(); + void enabledChanged(); + void sequenceChanged(); + void richTextLabelChanged(); + void mnemonicLabelChanged(); + void plainTextLabelChanged(); + void controlTypeChanged(); + void activeChanged(); + +private: + QWindow *window() const; + + void onAltPressed(); + void onAltReleased(); + + void calculateWeights(); + + // TODO: to have support for DIALOG_BUTTON_EXTRA_WEIGHT etc, a type enum should be exported + enum { + // Additional weight for first character in string + FIRST_CHARACTER_EXTRA_WEIGHT = 50, + // Additional weight for the beginning of a word + WORD_BEGINNING_EXTRA_WEIGHT = 50, + // Additional weight for a 'wanted' accelerator ie string with '&' + WANTED_ACCEL_EXTRA_WEIGHT = 150, + // Default weight for an 'action' widget (ie, pushbuttons) + ACTION_ELEMENT_WEIGHT = 50, + // Additional weight for the dialog buttons (large, we basically never want these reassigned) + DIALOG_BUTTON_EXTRA_WEIGHT = 300, + // Weight for FormLayout labels (low) + FORM_LABEL_WEIGHT = 20, + // Weight for Secondary controls which are considered less important (low) + SECONDARY_CONTROL_WEIGHT = 10, + // Default weight for menu items + MENU_ITEM_WEIGHT = 250, + }; + + // order word letters by weight + int m_weight = 0; + int m_baseWeight = 0; + ControlType m_controlType = SecondaryControl; + QMap m_weights; + + QString m_label; + QString m_actualRichTextLabel; + QString m_richTextLabel; + QString m_mnemonicLabel; + QKeySequence m_sequence; + bool m_enabled = true; + bool m_active = false; + + QPointer m_window; + + // global mapping of mnemonics + // TODO: map by QWindow + static QHash s_sequenceToObject; +}; + +QML_DECLARE_TYPEINFO(MnemonicAttached, QML_HAS_ATTACHED_PROPERTIES) + +#endif // MnemonicATTACHED_H diff --git a/src/overlayzstackingattached.cpp b/src/overlayzstackingattached.cpp new file mode 100644 index 0000000..e749a6d --- /dev/null +++ b/src/overlayzstackingattached.cpp @@ -0,0 +1,214 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "overlayzstackingattached.h" + +#include "loggingcategory.h" + +#include + +OverlayZStackingAttached::OverlayZStackingAttached(QObject *parent) + : QObject(parent) + , m_layer(defaultLayerForPopupType(parent)) + , m_parentPopup(nullptr) + , m_pending(false) +{ + Q_ASSERT(parent); + if (!isPopup(parent)) { + qCWarning(KirigamiLog) << "OverlayZStacking must be attached to a Popup"; + return; + } + + connect(parent, SIGNAL(parentChanged()), this, SLOT(updateParentPopup())); + connect(parent, SIGNAL(closed()), this, SLOT(dispatchPendingSignal())); + // Note: aboutToShow is too late, as QQuickPopup has already created modal + // dimmer based off current z index. +} + +OverlayZStackingAttached::~OverlayZStackingAttached() +{ +} + +qreal OverlayZStackingAttached::z() const +{ + if (!m_parentPopup) { + const_cast(this)->updateParentPopupSilent(); + } + + qreal layerZ = defaultZForLayer(m_layer); + qreal parentZ = parentPopupZ() + 1; + + return std::max(layerZ, parentZ); +} + +OverlayZStackingAttached::Layer OverlayZStackingAttached::layer() const +{ + return m_layer; +} + +void OverlayZStackingAttached::setLayer(Layer layer) +{ + if (m_layer == layer) { + return; + } + + m_layer = layer; + Q_EMIT layerChanged(); + enqueueSignal(); +} + +OverlayZStackingAttached *OverlayZStackingAttached::qmlAttachedProperties(QObject *object) +{ + return new OverlayZStackingAttached(object); +} + +void OverlayZStackingAttached::enqueueSignal() +{ + if (isVisible(parent())) { + m_pending = true; + } else { + Q_EMIT zChanged(); + } +} + +void OverlayZStackingAttached::dispatchPendingSignal() +{ + if (m_pending) { + m_pending = false; + Q_EMIT zChanged(); + } +} + +void OverlayZStackingAttached::updateParentPopup() +{ + const qreal oldZ = parentPopupZ(); + + updateParentPopupSilent(); + + if (oldZ != parentPopupZ()) { + enqueueSignal(); + } +} + +void OverlayZStackingAttached::updateParentPopupSilent() +{ + auto popup = findParentPopup(parent()); + setParentPopup(popup); +} + +void OverlayZStackingAttached::setParentPopup(QObject *parentPopup) +{ + if (m_parentPopup == parentPopup) { + return; + } + + if (m_parentPopup) { + disconnect(m_parentPopup.data(), SIGNAL(zChanged()), this, SLOT(enqueueSignal())); + } + + // Ideally, we would also connect to all the parent items' parentChanged() on the way to parent popup. + m_parentPopup = parentPopup; + + if (m_parentPopup) { + connect(m_parentPopup.data(), SIGNAL(zChanged()), this, SLOT(enqueueSignal())); + } +} + +qreal OverlayZStackingAttached::parentPopupZ() const +{ + if (m_parentPopup) { + return m_parentPopup->property("z").toReal(); + } + return -1; +} + +bool OverlayZStackingAttached::isVisible(const QObject *popup) +{ + if (!isPopup(popup)) { + return false; + } + + return popup->property("visible").toBool(); +} + +bool OverlayZStackingAttached::isPopup(const QObject *object) +{ + return object && object->inherits("QQuickPopup"); +} + +QObject *OverlayZStackingAttached::findParentPopup(const QObject *popup) +{ + auto item = findParentPopupItem(popup); + if (!item) { + return nullptr; + } + auto parentPopup = item->parent(); + if (!isPopup(popup)) { + return nullptr; + } + return parentPopup; +} + +QQuickItem *OverlayZStackingAttached::findParentPopupItem(const QObject *popup) +{ + if (!isPopup(popup)) { + return nullptr; + } + + QQuickItem *parentItem = popup->property("parent").value(); + if (!parentItem) { + return nullptr; + } + + QQuickItem *item = parentItem; + do { + if (item && item->inherits("QQuickPopupItem")) { + return item; + } + item = item->parentItem(); + } while (item); + + return nullptr; +} + +OverlayZStackingAttached::Layer OverlayZStackingAttached::defaultLayerForPopupType(const QObject *popup) +{ + if (popup) { + if (popup->inherits("QQuickDialog")) { + return Layer::Dialog; + } else if (popup->inherits("QQuickDrawer")) { + return Layer::Drawer; + } else if (popup->inherits("QQuickMenu")) { + return Layer::Menu; + } else if (popup->inherits("QQuickToolTip")) { + return Layer::ToolTip; + } + } + return DefaultLowest; +} + +qreal OverlayZStackingAttached::defaultZForLayer(Layer layer) +{ + switch (layer) { + case DefaultLowest: + return 0; + case Drawer: + return 100; + case FullScreen: + return 200; + case Dialog: + return 300; + case Menu: + return 400; + case Notification: + return 500; + case ToolTip: + return 600; + } + return 0; +} + +#include "moc_overlayzstackingattached.cpp" diff --git a/src/overlayzstackingattached.h b/src/overlayzstackingattached.h new file mode 100644 index 0000000..12ae340 --- /dev/null +++ b/src/overlayzstackingattached.h @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef OVERLAYZSTACKINGATTACHED_H +#define OVERLAYZSTACKINGATTACHED_H + +#include +#include + +#include + +class QQuickItem; + +/** + * This attached property manages z-index for stacking overlays relative to each other. + * + * When a popup is about to show, OverlayZStacking object kicks in, searches for the + * next nearest popup in the QtQuick hierarchy of items, and sets its z value to the + * biggest of two: current stacking value for its layer, or parent's z index + 1. + * This way OverlayZStacking algorithm ensures that a popup is always stacked higher + * than its logical parent popup, but also no lower than its siblings on the same + * logical layer. + * + * @code + * import QtQuick.Controls as QQC2 + * import org.kde.kirigami as Kirigami + * + * QQC2.Popup { + * Kirigami.OverlayZStacking.layer: Kirigami.OverlayZStacking.ToolTip + * z: Kirigami.OverlayZStacking.z + * } + * @endcode + * + * @since 6.0 + */ +class OverlayZStackingAttached : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_NAMED_ELEMENT(OverlayZStacking) + QML_UNCREATABLE("Cannot create objects of type OverlayZStacking, use it as an attached property") + QML_ATTACHED(OverlayZStackingAttached) + /** + * An optimal z-index that attachee popup should bind to. + */ + Q_PROPERTY(qreal z READ z NOTIFY zChanged FINAL) + + /** + * The logical stacking layer of attachee popup, akin to window manager's layers. + */ + Q_PROPERTY(Layer layer READ layer WRITE setLayer NOTIFY layerChanged FINAL) + +public: + enum Layer { + DefaultLowest = 0, + Drawer, + FullScreen, + Dialog, + Menu, + Notification, + ToolTip, + }; + Q_ENUM(Layer) + + explicit OverlayZStackingAttached(QObject *parent = nullptr); + ~OverlayZStackingAttached() override; + + qreal z() const; + + Layer layer() const; + void setLayer(Layer layer); + + // QML attached property + static OverlayZStackingAttached *qmlAttachedProperties(QObject *object); + +Q_SIGNALS: + void zChanged(); + void layerChanged(); + +private Q_SLOTS: + // Popup shall not change z index while being open, so if changes arrive, we defer it until closed. + void enqueueSignal(); + void dispatchPendingSignal(); + + void updateParentPopup(); + +private: + void updateParentPopupSilent(); + void setParentPopup(QObject *popup); + qreal parentPopupZ() const; + static bool isVisible(const QObject *popup); + static bool isPopup(const QObject *object); + static QObject *findParentPopup(const QObject *popup); + static QQuickItem *findParentPopupItem(const QObject *popup); + static Layer defaultLayerForPopupType(const QObject *popup); + static qreal defaultZForLayer(Layer layer); + + Layer m_layer = Layer::DefaultLowest; + QPointer m_parentPopup; + bool m_pending; +}; + +QML_DECLARE_TYPEINFO(OverlayZStackingAttached, QML_HAS_ATTACHED_PROPERTIES) + +#endif // OVERLAYZSTACKINGATTACHED_H diff --git a/src/pagepool.cpp b/src/pagepool.cpp new file mode 100644 index 0000000..2b119a6 --- /dev/null +++ b/src/pagepool.cpp @@ -0,0 +1,299 @@ +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "pagepool.h" + +#include +#include +#include +#include +#include + +#include "loggingcategory.h" + +PagePool::PagePool(QObject *parent) + : QObject(parent) +{ +} + +PagePool::~PagePool() +{ +} + +QUrl PagePool::lastLoadedUrl() const +{ + return m_lastLoadedUrl; +} + +QQuickItem *PagePool::lastLoadedItem() const +{ + return m_lastLoadedItem; +} + +QList PagePool::items() const +{ + return m_itemForUrl.values(); +} + +QList PagePool::urls() const +{ + return m_urlForItem.values(); +} + +void PagePool::setCachePages(bool cache) +{ + if (cache == m_cachePages) { + return; + } + + if (cache) { + clear(); + } + + m_cachePages = cache; + Q_EMIT cachePagesChanged(); +} + +bool PagePool::cachePages() const +{ + return m_cachePages; +} + +QQuickItem *PagePool::loadPage(const QString &url, QJSValue callback) +{ + return loadPageWithProperties(url, QVariantMap(), callback); +} + +QQuickItem *PagePool::loadPageWithProperties(const QString &url, const QVariantMap &properties, QJSValue callback) +{ + const auto engine = qmlEngine(this); + Q_ASSERT(engine); + + const QUrl actualUrl = resolvedUrl(url); + + auto found = m_itemForUrl.find(actualUrl); + if (found != m_itemForUrl.end()) { + m_lastLoadedUrl = found.key(); + m_lastLoadedItem = found.value(); + + if (callback.isCallable()) { + QJSValueList args = {engine->newQObject(found.value())}; + callback.call(args); + Q_EMIT lastLoadedUrlChanged(); + Q_EMIT lastLoadedItemChanged(); + // We could return the item, but for api coherence return null + return nullptr; + + } else { + Q_EMIT lastLoadedUrlChanged(); + Q_EMIT lastLoadedItemChanged(); + return found.value(); + } + } + + QQmlComponent *component = m_componentForUrl.value(actualUrl); + + if (!component) { + component = new QQmlComponent(engine, actualUrl, QQmlComponent::PreferSynchronous); + } + + if (component->status() == QQmlComponent::Loading) { + if (!callback.isCallable()) { + component->deleteLater(); + m_componentForUrl.remove(actualUrl); + return nullptr; + } + + connect(component, &QQmlComponent::statusChanged, this, [this, engine, component, callback, properties](QQmlComponent::Status status) mutable { + if (status != QQmlComponent::Ready) { + qCWarning(KirigamiLog) << component->errors(); + m_componentForUrl.remove(component->url()); + component->deleteLater(); + return; + } + QQuickItem *item = createFromComponent(component, properties); + if (item) { + QJSValueList args = {engine->newQObject(item)}; + callback.call(args); + } + + if (m_cachePages) { + component->deleteLater(); + } else { + m_componentForUrl[component->url()] = component; + } + }); + + return nullptr; + + } else if (component->status() != QQmlComponent::Ready) { + qCWarning(KirigamiLog) << component->errors(); + return nullptr; + } + + QQuickItem *item = createFromComponent(component, properties); + if (!item) { + return nullptr; + } + + if (m_cachePages) { + component->deleteLater(); + QQmlEngine::setObjectOwnership(item, QQmlEngine::CppOwnership); + m_itemForUrl[component->url()] = item; + m_urlForItem[item] = component->url(); + Q_EMIT itemsChanged(); + Q_EMIT urlsChanged(); + + } else { + m_componentForUrl[component->url()] = component; + QQmlEngine::setObjectOwnership(item, QQmlEngine::JavaScriptOwnership); + } + + m_lastLoadedUrl = actualUrl; + m_lastLoadedItem = item; + Q_EMIT lastLoadedUrlChanged(); + Q_EMIT lastLoadedItemChanged(); + + if (callback.isCallable()) { + QJSValueList args = {engine->newQObject(item)}; + callback.call(args); + // We could return the item, but for api coherence return null + return nullptr; + } + return item; +} + +QQuickItem *PagePool::createFromComponent(QQmlComponent *component, const QVariantMap &properties) +{ + const auto ctx = qmlContext(this); + Q_ASSERT(ctx); + + QObject *obj = component->createWithInitialProperties(properties, ctx); + + if (!obj || component->isError()) { + qCWarning(KirigamiLog) << component->errors(); + if (obj) { + obj->deleteLater(); + } + return nullptr; + } + + QQuickItem *item = qobject_cast(obj); + if (!item) { + qCWarning(KirigamiLog) << "Storing Non-QQuickItem in PagePool not supported"; + obj->deleteLater(); + return nullptr; + } + + return item; +} + +QUrl PagePool::resolvedUrl(const QString &stringUrl) const +{ + const auto ctx = qmlContext(this); + Q_ASSERT(ctx); + + QUrl actualUrl(stringUrl); + if (actualUrl.scheme().isEmpty()) { + actualUrl = ctx->resolvedUrl(actualUrl); + } + return actualUrl; +} + +bool PagePool::isLocalUrl(const QUrl &url) +{ + return url.isLocalFile() || url.scheme().isEmpty() || url.scheme() == QStringLiteral("qrc"); +} + +QUrl PagePool::urlForPage(QQuickItem *item) const +{ + return m_urlForItem.value(item); +} + +QQuickItem *PagePool::pageForUrl(const QUrl &url) const +{ + return m_itemForUrl.value(resolvedUrl(url.toString()), nullptr); +} + +bool PagePool::contains(const QVariant &page) const +{ + if (page.canConvert()) { + return m_urlForItem.contains(page.value()); + + } else if (page.canConvert()) { + const QUrl actualUrl = resolvedUrl(page.value()); + return m_itemForUrl.contains(actualUrl); + + } else { + return false; + } +} + +void PagePool::deletePage(const QVariant &page) +{ + if (!contains(page)) { + return; + } + + QQuickItem *item; + if (page.canConvert()) { + item = page.value(); + } else if (page.canConvert()) { + QString url = page.value(); + if (url.isEmpty()) { + return; + } + const QUrl actualUrl = resolvedUrl(page.value()); + + item = m_itemForUrl.value(actualUrl); + } else { + return; + } + + if (!item) { + return; + } + + const QUrl url = m_urlForItem.value(item); + + if (url.isEmpty()) { + return; + } + + m_itemForUrl.remove(url); + m_urlForItem.remove(item); + item->deleteLater(); + + Q_EMIT itemsChanged(); + Q_EMIT urlsChanged(); +} + +void PagePool::clear() +{ + for (const auto &component : std::as_const(m_componentForUrl)) { + component->deleteLater(); + } + m_componentForUrl.clear(); + + for (const auto &item : std::as_const(m_itemForUrl)) { + // items that had been deparented are safe to delete + if (!item->parentItem()) { + item->deleteLater(); + } + QQmlEngine::setObjectOwnership(item, QQmlEngine::JavaScriptOwnership); + } + m_itemForUrl.clear(); + m_urlForItem.clear(); + m_lastLoadedUrl = QUrl(); + m_lastLoadedItem = nullptr; + + Q_EMIT lastLoadedUrlChanged(); + Q_EMIT lastLoadedItemChanged(); + Q_EMIT itemsChanged(); + Q_EMIT urlsChanged(); +} + +#include "moc_pagepool.cpp" diff --git a/src/pagepool.h b/src/pagepool.h new file mode 100644 index 0000000..19364f1 --- /dev/null +++ b/src/pagepool.h @@ -0,0 +1,147 @@ +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ +#pragma once + +#include +#include +#include + +/** + * A Pool of Page items, pages will be unique per url and the items + * will be kept around unless explicitly deleted. + * Instances are C++ owned and can be deleted only manually using deletePage() + * Instance are unique per url: if you need 2 different instance for a page + * url, you should instantiate them in the traditional way + * or use a different PagePool instance. + * + * @see org::kde::kirigami::PagePoolAction + */ +class PagePool : public QObject +{ + Q_OBJECT + QML_ELEMENT + /** + * The last url that was loaded with @loadPage. Useful if you need + * to have a "checked" state to buttons or list items that + * load the page when clicked. + */ + Q_PROPERTY(QUrl lastLoadedUrl READ lastLoadedUrl NOTIFY lastLoadedUrlChanged FINAL) + + /** + * The last item that was loaded with @loadPage. + */ + Q_PROPERTY(QQuickItem *lastLoadedItem READ lastLoadedItem NOTIFY lastLoadedItemChanged FINAL) + + /** + * All items loaded/managed by the PagePool. + * @since 5.84 + */ + Q_PROPERTY(QList items READ items NOTIFY itemsChanged FINAL) + + /** + * All page URLs loaded/managed by the PagePool. + * @since 5.84 + */ + Q_PROPERTY(QList urls READ urls NOTIFY urlsChanged FINAL) + + /** + * If true (default) the pages will be kept around, will have C++ ownership and + * only one instance per page will be created. + * If false the pages will have Javascript ownership (thus deleted on pop by the + * page stacks) and each call to loadPage will create a new page instance. When + * cachePages is false, Components get cached nevertheless. + */ + Q_PROPERTY(bool cachePages READ cachePages WRITE setCachePages NOTIFY cachePagesChanged FINAL) + +public: + PagePool(QObject *parent = nullptr); + ~PagePool() override; + + QUrl lastLoadedUrl() const; + QQuickItem *lastLoadedItem() const; + QList items() const; + QList urls() const; + + void setCachePages(bool cache); + bool cachePages() const; + + /** + * Returns the instance of the item defined in the QML file identified + * by url, only one instance will be made per url if cachePAges is true. + * If the url is remote (i.e. http) don't rely on the return value but + * us the async callback instead. + * + * @param url full url of the item: it can be a well formed Url, an + * absolute path or a relative one to the path of the qml file the + * PagePool is instantiated from + * @param callback If we are loading a remote url, we can't have the + * item immediately but will be passed as a parameter to the provided + * callback. Normally, don't set a callback, use it only in case of + * remote urls + * @returns the page instance that will have been created if necessary. + * If the url is remote it will return null, as well will return null + * if the callback has been provided + */ + Q_INVOKABLE QQuickItem *loadPage(const QString &url, QJSValue callback = QJSValue()); + + Q_INVOKABLE QQuickItem *loadPageWithProperties(const QString &url, const QVariantMap &properties, QJSValue callback = QJSValue()); + + /** + * @returns The url of the page for the given instance, empty if there is no correspondence + */ + Q_INVOKABLE QUrl urlForPage(QQuickItem *item) const; + + /** + * @returns The page associated with a given URL, nullptr if there is no correspondence + */ + Q_INVOKABLE QQuickItem *pageForUrl(const QUrl &url) const; + + /** + * @returns true if the is managed by the PagePool + * @param the page can be either a QQuickItem or an url + */ + Q_INVOKABLE bool contains(const QVariant &page) const; + + /** + * Deletes the page (only if is managed by the pool. + * @param page either the url or the instance of the page + */ + Q_INVOKABLE void deletePage(const QVariant &page); + + /** + * @returns full url from an absolute or relative path + */ + Q_INVOKABLE QUrl resolvedUrl(const QString &file) const; + + /** + * @returns true if the url identifies a local resource (local file or a file inside Qt's resource system). + * False if the url points to a network location + */ + Q_INVOKABLE bool isLocalUrl(const QUrl &url); + + /** + * Deletes all pages managed by the pool. + */ + Q_INVOKABLE void clear(); + +Q_SIGNALS: + void lastLoadedUrlChanged(); + void lastLoadedItemChanged(); + void itemsChanged(); + void urlsChanged(); + void cachePagesChanged(); + +private: + QQuickItem *createFromComponent(QQmlComponent *component, const QVariantMap &properties); + + QUrl m_lastLoadedUrl; + QPointer m_lastLoadedItem; + QHash m_itemForUrl; + QHash m_componentForUrl; + QHash m_urlForItem; + + bool m_cachePages = true; +}; diff --git a/src/platform/CMakeLists.txt b/src/platform/CMakeLists.txt new file mode 100644 index 0000000..390d0dc --- /dev/null +++ b/src/platform/CMakeLists.txt @@ -0,0 +1,172 @@ +add_library(KirigamiPlatform) +add_library(KF6::KirigamiPlatform ALIAS KirigamiPlatform) + +ecm_add_qml_module(KirigamiPlatform + URI "org.kde.kirigami.platform" + VERSION 2.0 + GENERATE_PLUGIN_SOURCE + INSTALLED_PLUGIN_TARGET KF6::KirigamiPlatformplugin + DEPENDENCIES QtQuick +) + +set_target_properties(KirigamiPlatform PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION 6 + EXPORT_NAME "KirigamiPlatform" +) + +target_sources(KirigamiPlatform PRIVATE + platformtheme.cpp + platformtheme.h + basictheme.cpp + basictheme_p.h + inputmethod.cpp + inputmethod.h + platformpluginfactory.cpp + platformpluginfactory.h + tabletmodewatcher.cpp + tabletmodewatcher.h + settings.cpp + settings.h + smoothscrollwatcher.cpp + smoothscrollwatcher.h + styleselector.cpp + styleselector.h + units.cpp + units.h + virtualkeyboardwatcher.cpp + virtualkeyboardwatcher.h + colorutils.cpp + colorutils.h +) + +set(libkirigami_extra_sources "") + +if (WITH_DBUS) + set_source_files_properties(org.freedesktop.portal.Settings.xml PROPERTIES INCLUDE dbustypes.h) + qt_add_dbus_interface(libkirigami_extra_sources org.freedesktop.portal.Settings.xml settings_interface) + set(LIBKIRIGAMKI_EXTRA_LIBS Qt6::DBus) +endif() + +target_sources(KirigamiPlatform PRIVATE ${libkirigami_extra_sources}) + +ecm_qt_declare_logging_category(KirigamiPlatform + HEADER kirigamiplatform_logging.h + IDENTIFIER KirigamiPlatform + CATEGORY_NAME kf.kirigami.platform + DESCRIPTION "Kirigami Platform" + DEFAULT_SEVERITY Warning + EXPORT KIRIGAMI +) + +ecm_setup_version(PROJECT + VARIABLE_PREFIX KIRIGAMIPLATFORM + VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/kirigamiplatform_version.h" + PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF6KirigamiPlatformConfigVersion.cmake" + SOVERSION 6 +) + +ecm_generate_export_header(KirigamiPlatform + VERSION ${PROJECT_VERSION} + BASE_NAME KirigamiPlatform + USE_VERSION_HEADER + DEPRECATION_VERSIONS +) + +target_include_directories(KirigamiPlatform + PUBLIC + "$" + "$" + "$" +) + +target_link_libraries(KirigamiPlatform + PUBLIC + Qt6::Core + Qt6::Qml + Qt6::Quick + PRIVATE + Qt6::GuiPrivate + Qt6::QuickControls2 + ${LIBKIRIGAMKI_EXTRA_LIBS} +) + +ecm_generate_headers(KirigamiPlatform_CamelCase_HEADERS + HEADER_NAMES + PlatformTheme + PlatformPluginFactory + StyleSelector + TabletModeWatcher + Units + VirtualKeyboardWatcher + REQUIRED_HEADERS KirigamiPlatform_HEADERS +) + +configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/KF6KirigamiPlatformConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/KF6KirigamiPlatformConfig.cmake" + INSTALL_DESTINATION ${KDE_INSTALL_CMAKEPACKAGEDIR}/KF6KirigamiPlatform + PATH_VARS CMAKE_INSTALL_PREFIX +) + +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/KF6KirigamiPlatformConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/KF6KirigamiPlatformConfigVersion.cmake" + DESTINATION ${KDE_INSTALL_CMAKEPACKAGEDIR}/KF6KirigamiPlatform + COMPONENT Devel +) + +install(TARGETS KirigamiPlatform EXPORT KF6KirigamiPlatformTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS}) + +install(EXPORT KF6KirigamiPlatformTargets + DESTINATION ${KDE_INSTALL_CMAKEPACKAGEDIR}/KF6KirigamiPlatform + FILE KF6KirigamiPlatformTargets.cmake + NAMESPACE KF6:: +) + +install(FILES + ${KirigamiPlatform_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/kirigamiplatform_export.h + ${CMAKE_CURRENT_BINARY_DIR}/kirigamiplatform_version.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/Kirigami/Platform # prefix matching C++ namespace + COMPONENT Devel +) +install(FILES + ${KirigamiPlatform_CamelCase_HEADERS} + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/Kirigami/Platform # prefix matching C++ namespace + COMPONENT Devel +) + +ecm_qt_install_logging_categories( + EXPORT KIRIGAMI + FILE kirigami.categories + DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR} +) + +ecm_finalize_qml_module(KirigamiPlatform EXPORT KF6KirigamiPlatformTargets) + +if(BUILD_QCH) + ecm_add_qch( + KF6Kirigami_QCH + NAME KirigamiPlatform + BASE_NAME KF6KirigamiPlatform + VERSION ${KF_VERSION} + ORG_DOMAIN org.kde + SOURCES # using only public headers, to cover only public API + platformpluginfactory.h + platformtheme.h + tabletmodewatcher.h + units.h + virtualkeyboardwatcher.h + MD_MAINPAGE "${CMAKE_CURRENT_SOURCE_DIR}/README.md" + LINK_QCHS + Qt6Core_QCH + BLANK_MACROS + KIRIGAMIPLATFORM_EXPORT + KIRIGAMIPLATFORM_DEPRECATED + TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} + QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} + COMPONENT Devel + ) +endif() + diff --git a/src/platform/KF6KirigamiPlatformConfig.cmake.in b/src/platform/KF6KirigamiPlatformConfig.cmake.in new file mode 100644 index 0000000..9d78e00 --- /dev/null +++ b/src/platform/KF6KirigamiPlatformConfig.cmake.in @@ -0,0 +1,14 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(Qt6Core @REQUIRED_QT_VERSION@) +find_dependency(Qt6Qml @REQUIRED_QT_VERSION@) +find_dependency(Qt6Quick @REQUIRED_QT_VERSION@) + +# Any changes in this ".cmake" file will be overwritten by CMake, the source is the ".cmake.in" file. + +include("${CMAKE_CURRENT_LIST_DIR}/KF6KirigamiPlatformTargets.cmake") + +set(KirigamiPlatform_INSTALL_PREFIX "@PACKAGE_CMAKE_INSTALL_PREFIX@") + +@PACKAGE_INCLUDE_QCHTARGETS@ diff --git a/src/platform/README.md b/src/platform/README.md new file mode 100644 index 0000000..2f7dd4c --- /dev/null +++ b/src/platform/README.md @@ -0,0 +1,12 @@ +# Kirigami Platform Submodule + +This module contains platform integration types. + +# What Goes Here + +The following criteria should be used to determine if a type belongs here: + +- No visual items. +- No dependencies on other Kirigami submodules. +- Types that help expose underlying platform properties like colors, device + state, etc. diff --git a/src/platform/basictheme.cpp b/src/platform/basictheme.cpp new file mode 100644 index 0000000..05ae84d --- /dev/null +++ b/src/platform/basictheme.cpp @@ -0,0 +1,212 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "basictheme_p.h" +#include "styleselector.h" + +#include +#include + +#include "kirigamiplatform_logging.h" + +namespace Kirigami +{ +namespace Platform +{ + +BasicThemeDefinition::BasicThemeDefinition(QObject *parent) + : QObject(parent) +{ + defaultFont = qGuiApp->font(); + + smallFont = qGuiApp->font(); + smallFont.setPointSize(smallFont.pointSize() - 2); +} + +void BasicThemeDefinition::syncToQml(PlatformTheme *object) +{ + auto item = qobject_cast(object->parent()); + if (item && qmlAttachedPropertiesObject(item, false) == object) { + Q_EMIT sync(item); + } +} + +BasicThemeInstance::BasicThemeInstance(QObject *parent) + : QObject(parent) +{ +} + +BasicThemeDefinition &BasicThemeInstance::themeDefinition(QQmlEngine *engine) +{ + if (m_themeDefinition) { + return *m_themeDefinition; + } + + if (!engine) { + qCWarning(KirigamiPlatform) << "No QML engine found, using default Basic theme."; + m_themeDefinition = std::make_unique(); + } else { + auto themeUrl = StyleSelector::componentUrl(QStringLiteral("Theme.qml")); + QQmlComponent component(engine); + component.loadUrl(themeUrl); + + if (auto themeDefinition = qobject_cast(component.create())) { + m_themeDefinition.reset(themeDefinition); + } else { + const auto errors = component.errors(); + for (auto error : errors) { + qCWarning(KirigamiPlatform) << error.toString(); + } + + qCWarning(KirigamiPlatform) << "Invalid Theme file, using default Basic theme."; + m_themeDefinition = std::make_unique(); + } + } + + connect(m_themeDefinition.get(), &BasicThemeDefinition::changed, this, &BasicThemeInstance::onDefinitionChanged); + + return *m_themeDefinition; +} + +void BasicThemeInstance::onDefinitionChanged() +{ + for (auto watcher : std::as_const(watchers)) { + watcher->sync(); + } +} + +Q_GLOBAL_STATIC(BasicThemeInstance, basicThemeInstance) + +BasicTheme::BasicTheme(QObject *parent) + : PlatformTheme(parent) +{ + basicThemeInstance()->watchers.append(this); + + sync(); +} + +BasicTheme::~BasicTheme() +{ + basicThemeInstance()->watchers.removeOne(this); +} + +void BasicTheme::sync() +{ + PlatformThemeChangeTracker tracker{this}; + + auto &definition = basicThemeInstance()->themeDefinition(qmlEngine(parent())); + + switch (colorSet()) { + case BasicTheme::Button: + setTextColor(tint(definition.buttonTextColor)); + setBackgroundColor(tint(definition.buttonBackgroundColor)); + setAlternateBackgroundColor(tint(definition.buttonAlternateBackgroundColor)); + setHoverColor(tint(definition.buttonHoverColor)); + setFocusColor(tint(definition.buttonFocusColor)); + break; + case BasicTheme::View: + setTextColor(tint(definition.viewTextColor)); + setBackgroundColor(tint(definition.viewBackgroundColor)); + setAlternateBackgroundColor(tint(definition.viewAlternateBackgroundColor)); + setHoverColor(tint(definition.viewHoverColor)); + setFocusColor(tint(definition.viewFocusColor)); + break; + case BasicTheme::Selection: + setTextColor(tint(definition.selectionTextColor)); + setBackgroundColor(tint(definition.selectionBackgroundColor)); + setAlternateBackgroundColor(tint(definition.selectionAlternateBackgroundColor)); + setHoverColor(tint(definition.selectionHoverColor)); + setFocusColor(tint(definition.selectionFocusColor)); + break; + case BasicTheme::Tooltip: + setTextColor(tint(definition.tooltipTextColor)); + setBackgroundColor(tint(definition.tooltipBackgroundColor)); + setAlternateBackgroundColor(tint(definition.tooltipAlternateBackgroundColor)); + setHoverColor(tint(definition.tooltipHoverColor)); + setFocusColor(tint(definition.tooltipFocusColor)); + break; + case BasicTheme::Complementary: + setTextColor(tint(definition.complementaryTextColor)); + setBackgroundColor(tint(definition.complementaryBackgroundColor)); + setAlternateBackgroundColor(tint(definition.complementaryAlternateBackgroundColor)); + setHoverColor(tint(definition.complementaryHoverColor)); + setFocusColor(tint(definition.complementaryFocusColor)); + break; + case BasicTheme::Window: + default: + setTextColor(tint(definition.textColor)); + setBackgroundColor(tint(definition.backgroundColor)); + setAlternateBackgroundColor(tint(definition.alternateBackgroundColor)); + setHoverColor(tint(definition.hoverColor)); + setFocusColor(tint(definition.focusColor)); + break; + } + + setDisabledTextColor(tint(definition.disabledTextColor)); + setHighlightColor(tint(definition.highlightColor)); + setHighlightedTextColor(tint(definition.highlightedTextColor)); + setActiveTextColor(tint(definition.activeTextColor)); + setActiveBackgroundColor(tint(definition.activeBackgroundColor)); + setLinkColor(tint(definition.linkColor)); + setLinkBackgroundColor(tint(definition.linkBackgroundColor)); + setVisitedLinkColor(tint(definition.visitedLinkColor)); + setVisitedLinkBackgroundColor(tint(definition.visitedLinkBackgroundColor)); + setNegativeTextColor(tint(definition.negativeTextColor)); + setNegativeBackgroundColor(tint(definition.negativeBackgroundColor)); + setNeutralTextColor(tint(definition.neutralTextColor)); + setNeutralBackgroundColor(tint(definition.neutralBackgroundColor)); + setPositiveTextColor(tint(definition.positiveTextColor)); + setPositiveBackgroundColor(tint(definition.positiveBackgroundColor)); + + setDefaultFont(definition.defaultFont); + setSmallFont(definition.smallFont); +} + +bool BasicTheme::event(QEvent *event) +{ + if (event->type() == PlatformThemeEvents::DataChangedEvent::type) { + sync(); + } + + if (event->type() == PlatformThemeEvents::ColorSetChangedEvent::type) { + sync(); + } + + if (event->type() == PlatformThemeEvents::ColorGroupChangedEvent::type) { + sync(); + } + + if (event->type() == PlatformThemeEvents::ColorChangedEvent::type) { + basicThemeInstance()->themeDefinition(qmlEngine(parent())).syncToQml(this); + } + + if (event->type() == PlatformThemeEvents::FontChangedEvent::type) { + basicThemeInstance()->themeDefinition(qmlEngine(parent())).syncToQml(this); + } + + return PlatformTheme::event(event); +} + +QColor BasicTheme::tint(const QColor &color) +{ + if (QQuickItem *item = qobject_cast(parent()); item && !item->isEnabled()) { + return QColor::fromHsvF(color.hueF(), color.saturationF() * 0.5, color.valueF() * 0.8); + } + switch (colorGroup()) { + case PlatformTheme::Inactive: + return QColor::fromHsvF(color.hueF(), color.saturationF() * 0.5, color.valueF()); + case PlatformTheme::Disabled: + return QColor::fromHsvF(color.hueF(), color.saturationF() * 0.5, color.valueF() * 0.8); + default: + return color; + } +} + +} +} + +#include "moc_basictheme_p.cpp" diff --git a/src/platform/basictheme_p.h b/src/platform/basictheme_p.h new file mode 100644 index 0000000..b334b03 --- /dev/null +++ b/src/platform/basictheme_p.h @@ -0,0 +1,197 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef BASICTHEME_H +#define BASICTHEME_H + +#include "platformtheme.h" + +#include "kirigamiplatform_export.h" + +namespace Kirigami +{ +namespace Platform +{ +class BasicTheme; + +class KIRIGAMIPLATFORM_EXPORT BasicThemeDefinition : public QObject +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QColor textColor MEMBER textColor NOTIFY changed FINAL) + Q_PROPERTY(QColor disabledTextColor MEMBER disabledTextColor NOTIFY changed FINAL) + + Q_PROPERTY(QColor highlightColor MEMBER highlightColor NOTIFY changed FINAL) + Q_PROPERTY(QColor highlightedTextColor MEMBER highlightedTextColor NOTIFY changed FINAL) + Q_PROPERTY(QColor backgroundColor MEMBER backgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor alternateBackgroundColor MEMBER alternateBackgroundColor NOTIFY changed FINAL) + + Q_PROPERTY(QColor focusColor MEMBER focusColor NOTIFY changed FINAL) + Q_PROPERTY(QColor hoverColor MEMBER hoverColor NOTIFY changed FINAL) + + Q_PROPERTY(QColor activeTextColor MEMBER activeTextColor NOTIFY changed FINAL) + Q_PROPERTY(QColor activeBackgroundColor MEMBER activeBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor linkColor MEMBER linkColor NOTIFY changed FINAL) + Q_PROPERTY(QColor linkBackgroundColor MEMBER linkBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor visitedLinkColor MEMBER visitedLinkColor NOTIFY changed FINAL) + Q_PROPERTY(QColor visitedLinkBackgroundColor MEMBER visitedLinkBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor negativeTextColor MEMBER negativeTextColor NOTIFY changed FINAL) + Q_PROPERTY(QColor negativeBackgroundColor MEMBER negativeBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor neutralTextColor MEMBER neutralTextColor NOTIFY changed FINAL) + Q_PROPERTY(QColor neutralBackgroundColor MEMBER neutralBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor positiveTextColor MEMBER positiveTextColor NOTIFY changed FINAL) + Q_PROPERTY(QColor positiveBackgroundColor MEMBER positiveBackgroundColor NOTIFY changed FINAL) + + Q_PROPERTY(QColor buttonTextColor MEMBER buttonTextColor NOTIFY changed FINAL) + Q_PROPERTY(QColor buttonBackgroundColor MEMBER buttonBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor buttonAlternateBackgroundColor MEMBER buttonAlternateBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor buttonHoverColor MEMBER buttonHoverColor NOTIFY changed FINAL) + Q_PROPERTY(QColor buttonFocusColor MEMBER buttonFocusColor NOTIFY changed FINAL) + + Q_PROPERTY(QColor viewTextColor MEMBER viewTextColor NOTIFY changed FINAL) + Q_PROPERTY(QColor viewBackgroundColor MEMBER viewBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor viewAlternateBackgroundColor MEMBER viewAlternateBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor viewHoverColor MEMBER viewHoverColor NOTIFY changed FINAL) + Q_PROPERTY(QColor viewFocusColor MEMBER viewFocusColor NOTIFY changed FINAL) + + Q_PROPERTY(QColor selectionTextColor MEMBER selectionTextColor NOTIFY changed FINAL) + Q_PROPERTY(QColor selectionBackgroundColor MEMBER selectionBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor selectionAlternateBackgroundColor MEMBER selectionAlternateBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor selectionHoverColor MEMBER selectionHoverColor NOTIFY changed FINAL) + Q_PROPERTY(QColor selectionFocusColor MEMBER selectionFocusColor NOTIFY changed FINAL) + + Q_PROPERTY(QColor tooltipTextColor MEMBER tooltipTextColor NOTIFY changed FINAL) + Q_PROPERTY(QColor tooltipBackgroundColor MEMBER tooltipBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor tooltipAlternateBackgroundColor MEMBER tooltipAlternateBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor tooltipHoverColor MEMBER tooltipHoverColor NOTIFY changed FINAL) + Q_PROPERTY(QColor tooltipFocusColor MEMBER tooltipFocusColor NOTIFY changed FINAL) + + Q_PROPERTY(QColor complementaryTextColor MEMBER complementaryTextColor NOTIFY changed FINAL) + Q_PROPERTY(QColor complementaryBackgroundColor MEMBER complementaryBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor complementaryAlternateBackgroundColor MEMBER complementaryAlternateBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor complementaryHoverColor MEMBER complementaryHoverColor NOTIFY changed FINAL) + Q_PROPERTY(QColor complementaryFocusColor MEMBER complementaryFocusColor NOTIFY changed FINAL) + + Q_PROPERTY(QColor headerTextColor MEMBER headerTextColor NOTIFY changed FINAL) + Q_PROPERTY(QColor headerBackgroundColor MEMBER headerBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor headerAlternateBackgroundColor MEMBER headerAlternateBackgroundColor NOTIFY changed FINAL) + Q_PROPERTY(QColor headerHoverColor MEMBER headerHoverColor NOTIFY changed FINAL) + Q_PROPERTY(QColor headerFocusColor MEMBER headerFocusColor NOTIFY changed FINAL) + + Q_PROPERTY(QFont defaultFont MEMBER defaultFont NOTIFY changed FINAL) + Q_PROPERTY(QFont smallFont MEMBER smallFont NOTIFY changed FINAL) + +public: + explicit BasicThemeDefinition(QObject *parent = nullptr); + + virtual void syncToQml(PlatformTheme *object); + + QColor textColor{0x31363b}; + QColor disabledTextColor{0x31, 0x36, 0x3b, 0x99}; + + QColor highlightColor{0x2196F3}; + QColor highlightedTextColor{0xeff0fa}; + QColor backgroundColor{0xeff0f1}; + QColor alternateBackgroundColor{0xbdc3c7}; + + QColor focusColor{0x2196F3}; + QColor hoverColor{0x2196F3}; + + QColor activeTextColor{0x0176D3}; + QColor activeBackgroundColor{0x0176D3}; + QColor linkColor{0x2196F3}; + QColor linkBackgroundColor{0x2196F3}; + QColor visitedLinkColor{0x2196F3}; + QColor visitedLinkBackgroundColor{0x2196F3}; + QColor negativeTextColor{0xDA4453}; + QColor negativeBackgroundColor{0xDA4453}; + QColor neutralTextColor{0xF67400}; + QColor neutralBackgroundColor{0xF67400}; + QColor positiveTextColor{0x27AE60}; + QColor positiveBackgroundColor{0x27AE60}; + + QColor buttonTextColor{0x31363b}; + QColor buttonBackgroundColor{0xeff0f1}; + QColor buttonAlternateBackgroundColor{0xbdc3c7}; + QColor buttonHoverColor{0x2196F3}; + QColor buttonFocusColor{0x2196F3}; + + QColor viewTextColor{0x31363b}; + QColor viewBackgroundColor{0xfcfcfc}; + QColor viewAlternateBackgroundColor{0xeff0f1}; + QColor viewHoverColor{0x2196F3}; + QColor viewFocusColor{0x2196F3}; + + QColor selectionTextColor{0xeff0fa}; + QColor selectionBackgroundColor{0x2196F3}; + QColor selectionAlternateBackgroundColor{0x1d99f3}; + QColor selectionHoverColor{0x2196F3}; + QColor selectionFocusColor{0x2196F3}; + + QColor tooltipTextColor{0xeff0f1}; + QColor tooltipBackgroundColor{0x31363b}; + QColor tooltipAlternateBackgroundColor{0x4d4d4d}; + QColor tooltipHoverColor{0x2196F3}; + QColor tooltipFocusColor{0x2196F3}; + + QColor complementaryTextColor{0xeff0f1}; + QColor complementaryBackgroundColor{0x31363b}; + QColor complementaryAlternateBackgroundColor{0x3b4045}; + QColor complementaryHoverColor{0x2196F3}; + QColor complementaryFocusColor{0x2196F3}; + + QColor headerTextColor{0x232629}; + QColor headerBackgroundColor{0xe3e5e7}; + QColor headerAlternateBackgroundColor{0xeff0f1}; + QColor headerHoverColor{0x2196F3}; + QColor headerFocusColor{0x93cee9}; + + QFont defaultFont; + QFont smallFont; + + Q_SIGNAL void changed(); + Q_SIGNAL void sync(QQuickItem *object); +}; + +class BasicThemeInstance : public QObject +{ + Q_OBJECT + +public: + explicit BasicThemeInstance(QObject *parent = nullptr); + + BasicThemeDefinition &themeDefinition(QQmlEngine *engine); + + QList watchers; + +private: + void onDefinitionChanged(); + + std::unique_ptr m_themeDefinition; +}; + +class BasicTheme : public PlatformTheme +{ + Q_OBJECT + +public: + explicit BasicTheme(QObject *parent = nullptr); + ~BasicTheme() override; + + void sync(); + +protected: + bool event(QEvent *event) override; + +private: + QColor tint(const QColor &color); +}; + +} +} + +#endif // BASICTHEME_H diff --git a/src/platform/colorutils.cpp b/src/platform/colorutils.cpp new file mode 100644 index 0000000..5a3bc7d --- /dev/null +++ b/src/platform/colorutils.cpp @@ -0,0 +1,319 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "colorutils.h" + +#include +#include +#include +#include + +#include "kirigamiplatform_logging.h" + +ColorUtils::ColorUtils(QObject *parent) + : QObject(parent) +{ +} + +ColorUtils::Brightness ColorUtils::brightnessForColor(const QColor &color) +{ + auto luma = [](const QColor &color) { + return (0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue()) / 255; + }; + + return luma(color) > 0.5 ? ColorUtils::Brightness::Light : ColorUtils::Brightness::Dark; +} + +qreal ColorUtils::grayForColor(const QColor &color) +{ + return (0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue()) / 255; +} + +QColor ColorUtils::alphaBlend(const QColor &foreground, const QColor &background) +{ + const auto foregroundAlpha = foreground.alpha(); + const auto inverseForegroundAlpha = 0xff - foregroundAlpha; + const auto backgroundAlpha = background.alpha(); + + if (foregroundAlpha == 0x00) { + return background; + } + + if (backgroundAlpha == 0xff) { + return QColor::fromRgb((foregroundAlpha * foreground.red()) + (inverseForegroundAlpha * background.red()), + (foregroundAlpha * foreground.green()) + (inverseForegroundAlpha * background.green()), + (foregroundAlpha * foreground.blue()) + (inverseForegroundAlpha * background.blue()), + 0xff); + } else { + const auto inverseBackgroundAlpha = (backgroundAlpha * inverseForegroundAlpha) / 255; + const auto finalAlpha = foregroundAlpha + inverseBackgroundAlpha; + Q_ASSERT(finalAlpha != 0x00); + return QColor::fromRgb((foregroundAlpha * foreground.red()) + (inverseBackgroundAlpha * background.red()), + (foregroundAlpha * foreground.green()) + (inverseBackgroundAlpha * background.green()), + (foregroundAlpha * foreground.blue()) + (inverseBackgroundAlpha * background.blue()), + finalAlpha); + } +} + +QColor ColorUtils::linearInterpolation(const QColor &one, const QColor &two, double balance) +{ + auto linearlyInterpolateDouble = [](double one, double two, double factor) { + return one + (two - one) * factor; + }; + + // QColor returns -1 when hue is undefined, which happens whenever + // saturation is 0. When this happens, interpolation can go wrong so handle + // it by first trying to use the other color's hue and if that is also -1, + // just skip the hue interpolation by using 0 for both. + auto sourceHue = std::max(one.hueF() > 0.0 ? one.hueF() : two.hueF(), 0.0f); + auto targetHue = std::max(two.hueF() > 0.0 ? two.hueF() : one.hueF(), 0.0f); + + auto hue = std::fmod(linearlyInterpolateDouble(sourceHue, targetHue, balance), 1.0); + auto saturation = std::clamp(linearlyInterpolateDouble(one.saturationF(), two.saturationF(), balance), 0.0, 1.0); + auto value = std::clamp(linearlyInterpolateDouble(one.valueF(), two.valueF(), balance), 0.0, 1.0); + auto alpha = std::clamp(linearlyInterpolateDouble(one.alphaF(), two.alphaF(), balance), 0.0, 1.0); + + return QColor::fromHsvF(hue, saturation, value, alpha); +} + +// Some private things for the adjust, change, and scale properties +struct ParsedAdjustments { + double red = 0.0; + double green = 0.0; + double blue = 0.0; + + double hue = 0.0; + double saturation = 0.0; + double value = 0.0; + + double alpha = 0.0; +}; + +ParsedAdjustments parseAdjustments(const QJSValue &value) +{ + ParsedAdjustments parsed; + + auto checkProperty = [](const QJSValue &value, const QString &property) { + if (value.hasProperty(property)) { + auto val = value.property(property); + if (val.isNumber()) { + return QVariant::fromValue(val.toNumber()); + } + } + return QVariant(); + }; + + std::vector> items{{QStringLiteral("red"), parsed.red}, + {QStringLiteral("green"), parsed.green}, + {QStringLiteral("blue"), parsed.blue}, + // + {QStringLiteral("hue"), parsed.hue}, + {QStringLiteral("saturation"), parsed.saturation}, + {QStringLiteral("value"), parsed.value}, + // + {QStringLiteral("alpha"), parsed.alpha}}; + + for (const auto &item : items) { + auto val = checkProperty(value, item.first); + if (val.isValid()) { + item.second = val.toDouble(); + } + } + + if ((parsed.red || parsed.green || parsed.blue) && (parsed.hue || parsed.saturation || parsed.value)) { + qCCritical(KirigamiPlatform) << "It is an error to have both RGB and HSV values in an adjustment."; + } + + return parsed; +} + +QColor ColorUtils::adjustColor(const QColor &color, const QJSValue &adjustments) +{ + auto adjusts = parseAdjustments(adjustments); + + if (qBound(-360.0, adjusts.hue, 360.0) != adjusts.hue) { + qCCritical(KirigamiPlatform) << "Hue is out of bounds"; + } + if (qBound(-255.0, adjusts.red, 255.0) != adjusts.red) { + qCCritical(KirigamiPlatform) << "Red is out of bounds"; + } + if (qBound(-255.0, adjusts.green, 255.0) != adjusts.green) { + qCCritical(KirigamiPlatform) << "Green is out of bounds"; + } + if (qBound(-255.0, adjusts.blue, 255.0) != adjusts.blue) { + qCCritical(KirigamiPlatform) << "Green is out of bounds"; + } + if (qBound(-255.0, adjusts.saturation, 255.0) != adjusts.saturation) { + qCCritical(KirigamiPlatform) << "Saturation is out of bounds"; + } + if (qBound(-255.0, adjusts.value, 255.0) != adjusts.value) { + qCCritical(KirigamiPlatform) << "Value is out of bounds"; + } + if (qBound(-255.0, adjusts.alpha, 255.0) != adjusts.alpha) { + qCCritical(KirigamiPlatform) << "Alpha is out of bounds"; + } + + auto copy = color; + + if (adjusts.alpha) { + copy.setAlpha(qBound(0.0, copy.alpha() + adjusts.alpha, 255.0)); + } + + if (adjusts.red || adjusts.green || adjusts.blue) { + copy.setRed(qBound(0.0, copy.red() + adjusts.red, 255.0)); + copy.setGreen(qBound(0.0, copy.green() + adjusts.green, 255.0)); + copy.setBlue(qBound(0.0, copy.blue() + adjusts.blue, 255.0)); + } else if (adjusts.hue || adjusts.saturation || adjusts.value) { + copy.setHsv(std::fmod(copy.hue() + adjusts.hue, 360.0), + qBound(0.0, copy.saturation() + adjusts.saturation, 255.0), + qBound(0.0, copy.value() + adjusts.value, 255.0), + copy.alpha()); + } + + return copy; +} + +QColor ColorUtils::scaleColor(const QColor &color, const QJSValue &adjustments) +{ + auto adjusts = parseAdjustments(adjustments); + auto copy = color; + + if (qBound(-100.0, adjusts.red, 100.00) != adjusts.red) { + qCCritical(KirigamiPlatform) << "Red is out of bounds"; + } + if (qBound(-100.0, adjusts.green, 100.00) != adjusts.green) { + qCCritical(KirigamiPlatform) << "Green is out of bounds"; + } + if (qBound(-100.0, adjusts.blue, 100.00) != adjusts.blue) { + qCCritical(KirigamiPlatform) << "Blue is out of bounds"; + } + if (qBound(-100.0, adjusts.saturation, 100.00) != adjusts.saturation) { + qCCritical(KirigamiPlatform) << "Saturation is out of bounds"; + } + if (qBound(-100.0, adjusts.value, 100.00) != adjusts.value) { + qCCritical(KirigamiPlatform) << "Value is out of bounds"; + } + if (qBound(-100.0, adjusts.alpha, 100.00) != adjusts.alpha) { + qCCritical(KirigamiPlatform) << "Alpha is out of bounds"; + } + + if (adjusts.hue != 0) { + qCCritical(KirigamiPlatform) << "Hue cannot be scaled"; + } + + auto shiftToAverage = [](double current, double factor) { + auto scale = qBound(-100.0, factor, 100.0) / 100; + return current + (scale > 0 ? 255 - current : current) * scale; + }; + + if (adjusts.alpha) { + copy.setAlpha(qBound(0.0, shiftToAverage(copy.alpha(), adjusts.alpha), 255.0)); + } + + if (adjusts.red || adjusts.green || adjusts.blue) { + copy.setRed(qBound(0.0, shiftToAverage(copy.red(), adjusts.red), 255.0)); + copy.setGreen(qBound(0.0, shiftToAverage(copy.green(), adjusts.green), 255.0)); + copy.setBlue(qBound(0.0, shiftToAverage(copy.blue(), adjusts.blue), 255.0)); + } else { + copy.setHsv(copy.hue(), + qBound(0.0, shiftToAverage(copy.saturation(), adjusts.saturation), 255.0), + qBound(0.0, shiftToAverage(copy.value(), adjusts.value), 255.0), + copy.alpha()); + } + + return copy; +} + +QColor ColorUtils::tintWithAlpha(const QColor &targetColor, const QColor &tintColor, double alpha) +{ + qreal tintAlpha = tintColor.alphaF() * alpha; + qreal inverseAlpha = 1.0 - tintAlpha; + + if (qFuzzyCompare(tintAlpha, 1.0)) { + return tintColor; + } else if (qFuzzyIsNull(tintAlpha)) { + return targetColor; + } + + return QColor::fromRgbF(tintColor.redF() * tintAlpha + targetColor.redF() * inverseAlpha, + tintColor.greenF() * tintAlpha + targetColor.greenF() * inverseAlpha, + tintColor.blueF() * tintAlpha + targetColor.blueF() * inverseAlpha, + tintAlpha + inverseAlpha * targetColor.alphaF()); +} + +ColorUtils::XYZColor ColorUtils::colorToXYZ(const QColor &color) +{ + // http://wiki.nuaj.net/index.php/Color_Transforms#RGB_.E2.86.92_XYZ + qreal r = color.redF(); + qreal g = color.greenF(); + qreal b = color.blueF(); + // Apply gamma correction (i.e. conversion to linear-space) + auto correct = [](qreal &v) { + if (v > 0.04045) { + v = std::pow((v + 0.055) / 1.055, 2.4); + } else { + v = v / 12.92; + } + }; + + correct(r); + correct(g); + correct(b); + + // Observer. = 2°, Illuminant = D65 + const qreal x = r * 0.4124 + g * 0.3576 + b * 0.1805; + const qreal y = r * 0.2126 + g * 0.7152 + b * 0.0722; + const qreal z = r * 0.0193 + g * 0.1192 + b * 0.9505; + + return XYZColor{x, y, z}; +} + +ColorUtils::LabColor ColorUtils::colorToLab(const QColor &color) +{ + // First: convert to XYZ + const auto xyz = colorToXYZ(color); + + // Second: convert from XYZ to L*a*b + qreal x = xyz.x / 0.95047; // Observer= 2°, Illuminant= D65 + qreal y = xyz.y / 1.0; + qreal z = xyz.z / 1.08883; + + auto pivot = [](qreal &v) { + if (v > 0.008856) { + v = std::pow(v, 1.0 / 3.0); + } else { + v = (7.787 * v) + (16.0 / 116.0); + } + }; + + pivot(x); + pivot(y); + pivot(z); + + LabColor labColor; + labColor.l = std::max(0.0, (116 * y) - 16); + labColor.a = 500 * (x - y); + labColor.b = 200 * (y - z); + + return labColor; +} + +qreal ColorUtils::chroma(const QColor &color) +{ + LabColor labColor = colorToLab(color); + + // Chroma is hypotenuse of a and b + return sqrt(pow(labColor.a, 2) + pow(labColor.b, 2)); +} + +qreal ColorUtils::luminance(const QColor &color) +{ + const auto &xyz = colorToXYZ(color); + // Luminance is equal to Y + return xyz.y; +} + +#include "moc_colorutils.cpp" diff --git a/src/platform/colorutils.h b/src/platform/colorutils.h new file mode 100644 index 0000000..26a471b --- /dev/null +++ b/src/platform/colorutils.h @@ -0,0 +1,218 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include +#include + +#include "kirigamiplatform_export.h" + +/** + * Utilities for processing items to obtain colors and information useful for + * UIs that need to adjust to variable elements. + */ +class KIRIGAMIPLATFORM_EXPORT ColorUtils : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON +public: + /** + * Describes the contrast of an item. + */ + enum Brightness { + Dark, /**< The item is dark and requires a light foreground color to achieve readable contrast. */ + Light, /**< The item is light and requires a dark foreground color to achieve readable contrast. */ + }; + Q_ENUM(Brightness) + + explicit ColorUtils(QObject *parent = nullptr); + + /** + * Returns whether a color is bright or dark. + * + * @code{.qml} + * import QtQuick + * import org.kde.kirigami as Kirigami + * + * Kirigami.Heading { + * text: { + * if (Kirigami.ColorUtils.brightnessForColor("pink") == Kirigami.ColorUtils.Light) { + * return "The color is light" + * } else { + * return "The color is dark" + * } + * } + * } + * @endcode + * + * @since 5.69 + * @since org.kde.kirigami 2.12 + */ + Q_INVOKABLE ColorUtils::Brightness brightnessForColor(const QColor &color); + + /** + * Same Algorithm as brightnessForColor but returns a 0 to 1 value for an + * estimate of the equivalent gray light value (luma). + * 0 as full black, 1 as full white and 0.5 equivalent to a 50% gray. + * + * @since 5.81 + * @since org.kde.kirigami 2.16 + */ + Q_INVOKABLE qreal grayForColor(const QColor &color); + + /** + * Returns the result of overlaying the foreground color on the background + * color. + * + * @param foreground The color to overlay on the background. + * + * @param background The color to overlay the foreground on. + * + * @code{.qml} + * import QtQuick + * import org.kde.kirigami as Kirigami + * + * Rectangle { + * color: Kirigami.ColorUtils.alphaBlend(Qt.rgba(0, 0, 0, 0.5), Qt.rgba(1, 1, 1, 1)) + * } + * @endcode + * + * @since 5.69 + * @since org.kde.kirigami 2.12 + */ + Q_INVOKABLE QColor alphaBlend(const QColor &foreground, const QColor &background); + + /** + * Returns a linearly interpolated color between color one and color two. + * + * @param one The color to linearly interpolate from. + * + * @param two The color to linearly interpolate to. + * + * @param balance The balance between the two colors. 0.0 will return the + * first color, 1.0 will return the second color. Values beyond these bounds + * are valid, and will result in extrapolation. + * + * @code{.qml} + * import QtQuick + * import org.kde.kirigami as Kirigami + * + * Rectangle { + * color: Kirigami.ColorUtils.linearInterpolation("black", "white", 0.5) + * } + * @endcode + * + * @since 5.69 + * @since org.kde.kirigami 2.12 + */ + Q_INVOKABLE QColor linearInterpolation(const QColor &one, const QColor &two, double balance); + + /** + * Increases or decreases the properties of `color` by fixed amounts. + * + * @param color The color to adjust. + * + * @param adjustments The adjustments to apply to the color. + * + * @code{.js} + * { + * red: null, // Range: -255 to 255 + * green: null, // Range: -255 to 255 + * blue: null, // Range: -255 to 255 + * hue: null, // Range: -360 to 360 + * saturation: null, // Range: -255 to 255 + * value: null // Range: -255 to 255 + * alpha: null, // Range: -255 to 255 + * } + * @endcode + * + * @warning It is an error to adjust both RGB and HSV properties. + * + * @since 5.69 + * @since org.kde.kirigami 2.12 + */ + Q_INVOKABLE QColor adjustColor(const QColor &color, const QJSValue &adjustments); + + /** + * Smoothly scales colors. + * + * @param color The color to adjust. + * + * @param adjustments The adjustments to apply to the color. Each value must + * be between `-100.0` and `100.0`. This indicates how far the property should + * be scaled from its original to the maximum if positive or to the minimum if + * negative. + * + * @code{.js} + * { + * red: null + * green: null + * blue: null + * saturation: null + * value: null + * alpha: null + * } + * @endcode + * + * @warning It is an error to scale both RGB and HSV properties. + * + * @since 5.69 + * @since org.kde.kirigami 2.12 + */ + Q_INVOKABLE QColor scaleColor(const QColor &color, const QJSValue &adjustments); + + /** + * Tint a color using a separate alpha value. + * + * This does the same as Qt.tint() except that rather than using the tint + * color's alpha value, it uses a separate value that gets multiplied with + * the tint color's alpha. This avoids needing to create a new color just to + * adjust an alpha value. + * + * \param targetColor The color to tint. + * \param tintColor The color to tint with. + * \param alpha The amount of tinting to apply. + * + * \return The tinted color. + * + * \sa Qt.tint() + */ + Q_INVOKABLE QColor tintWithAlpha(const QColor &targetColor, const QColor &tintColor, double alpha); + + /** + * Returns the CIELAB chroma of the given color. + * + * CIELAB chroma may give a better quantification of how vibrant a color is compared to HSV saturation. + * + * \sa https://en.wikipedia.org/wiki/Colorfulness + * \sa https://en.wikipedia.org/wiki/CIELAB_color_space + */ + Q_INVOKABLE static qreal chroma(const QColor &color); + + struct XYZColor { + qreal x = 0; + qreal y = 0; + qreal z = 0; + }; + + struct LabColor { + qreal l = 0; + qreal a = 0; + qreal b = 0; + }; + + // Not for QML, returns the comvertion from srgb of a QColor and XYZ colorspace + static ColorUtils::XYZColor colorToXYZ(const QColor &color); + + // Not for QML, returns the comvertion from srgb of a QColor and Lab colorspace + static ColorUtils::LabColor colorToLab(const QColor &color); + + static qreal luminance(const QColor &color); +}; diff --git a/src/platform/dbustypes.h b/src/platform/dbustypes.h new file mode 100644 index 0000000..2357c46 --- /dev/null +++ b/src/platform/dbustypes.h @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2018-2019 Red Hat Inc + * SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + * + * SPDX-FileCopyrightText: 2018-2019 Jan Grulich + */ + +#pragma once + +#include +#include + +/// a{sa{sv}} +using VariantMapMap = QMap>; + +Q_DECLARE_METATYPE(VariantMapMap) diff --git a/src/platform/inputmethod.cpp b/src/platform/inputmethod.cpp new file mode 100644 index 0000000..54b9fb2 --- /dev/null +++ b/src/platform/inputmethod.cpp @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "inputmethod.h" + +#include "virtualkeyboardwatcher.h" + +namespace Kirigami +{ +namespace Platform +{ + +class KIRIGAMIPLATFORM_NO_EXPORT InputMethod::Private +{ +public: + bool available = false; + bool enabled = false; + bool active = false; + bool visible = false; +}; + +InputMethod::InputMethod(QObject *parent) + : QObject(parent) + , d(std::make_unique()) +{ + auto watcher = VirtualKeyboardWatcher::self(); + + connect(watcher, &VirtualKeyboardWatcher::availableChanged, this, [this]() { + d->available = VirtualKeyboardWatcher::self()->available(); + Q_EMIT availableChanged(); + }); + + connect(watcher, &VirtualKeyboardWatcher::enabledChanged, this, [this]() { + d->enabled = VirtualKeyboardWatcher::self()->enabled(); + Q_EMIT enabledChanged(); + }); + + connect(watcher, &VirtualKeyboardWatcher::activeChanged, this, [this]() { + d->active = VirtualKeyboardWatcher::self()->active(); + Q_EMIT activeChanged(); + }); + + connect(watcher, &VirtualKeyboardWatcher::visibleChanged, this, [this]() { + d->visible = VirtualKeyboardWatcher::self()->visible(); + Q_EMIT visibleChanged(); + }); + + connect(watcher, &VirtualKeyboardWatcher::willShowOnActiveChanged, this, [this]() { + Q_EMIT willShowOnActiveChanged(); + }); + + d->available = watcher->available(); + d->enabled = watcher->enabled(); + d->active = watcher->active(); + d->visible = watcher->visible(); +} + +InputMethod::~InputMethod() = default; + +bool InputMethod::available() const +{ + return d->available; +} + +bool InputMethod::enabled() const +{ + return d->enabled; +} + +bool InputMethod::active() const +{ + return d->active; +} + +bool InputMethod::visible() const +{ + return d->visible; +} + +bool InputMethod::willShowOnActive() const +{ + return VirtualKeyboardWatcher::self()->willShowOnActive(); +} + +} +} + +#include "moc_inputmethod.cpp" diff --git a/src/platform/inputmethod.h b/src/platform/inputmethod.h new file mode 100644 index 0000000..981d196 --- /dev/null +++ b/src/platform/inputmethod.h @@ -0,0 +1,97 @@ +/* + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef INPUTMETHOD_H +#define INPUTMETHOD_H + +#include + +#include +#include + +#include "kirigamiplatform_export.h" + +namespace Kirigami +{ +namespace Platform +{ + +/** + * This exposes information about the current used input method. + */ +class KIRIGAMIPLATFORM_EXPORT InputMethod : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + InputMethod(QObject *parent = nullptr); + ~InputMethod() override; + + /** + * Is an input method available? + * + * This will be true if there is an input method available. When it is + * false it means there's no special input method configured and input + * happens directly through keyboard events. + */ + Q_PROPERTY(bool available READ available NOTIFY availableChanged FINAL) + bool available() const; + Q_SIGNAL void availableChanged(); + + /** + * Is the current input method enabled? + * + * If this is false, that means the input method is available but not in use. + */ + Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged FINAL) + bool enabled() const; + Q_SIGNAL void enabledChanged(); + + /** + * Is the current input method active? + * + * What active means depends on the type of input method. In case of a + * virtual keyboard for example, it would mean the virtual keyboard is + * visible. + */ + Q_PROPERTY(bool active READ active NOTIFY activeChanged FINAL) + bool active() const; + Q_SIGNAL void activeChanged(); + + /** + * Is the current input method visible? + * + * For some input methods this will match \ref active however for others this + * may differ. + */ + Q_PROPERTY(bool visible READ visible NOTIFY visibleChanged FINAL) + bool visible() const; + Q_SIGNAL void visibleChanged(); + + /** + * Will the input method be shown when a text input field gains focus? + * + * This is intended to be used to decide whether to give an input field + * default focus. For certain input methods, like virtual keyboards, it may + * not be desirable to show it by default. For example, having a search + * field focused on application startup may cause the virtual keyboard to + * show, greatly reducing the available application space. + */ + Q_PROPERTY(bool willShowOnActive READ willShowOnActive NOTIFY willShowOnActiveChanged FINAL) + bool willShowOnActive() const; + Q_SIGNAL void willShowOnActiveChanged(); + +private: + class Private; + const std::unique_ptr d; +}; + +} +} + +#endif // INPUTMETHOD_H diff --git a/src/platform/org.freedesktop.portal.Settings.xml b/src/platform/org.freedesktop.portal.Settings.xml new file mode 100644 index 0000000..5413538 --- /dev/null +++ b/src/platform/org.freedesktop.portal.Settings.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/platform/platformpluginfactory.cpp b/src/platform/platformpluginfactory.cpp new file mode 100644 index 0000000..f4179d1 --- /dev/null +++ b/src/platform/platformpluginfactory.cpp @@ -0,0 +1,106 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "platformpluginfactory.h" + +#include +#include +#include +#include +#include + +#include "kirigamiplatform_logging.h" + +namespace Kirigami +{ +namespace Platform +{ + +PlatformPluginFactory::PlatformPluginFactory(QObject *parent) + : QObject(parent) +{ +} + +PlatformPluginFactory::~PlatformPluginFactory() = default; + +PlatformPluginFactory *PlatformPluginFactory::findPlugin(const QString &preferredName) +{ + static QHash factories = QHash(); + + QString pluginName = preferredName.isEmpty() ? QQuickStyle::name() : preferredName; + // check for the plugin only once: it's an heavy operation + if (auto it = factories.constFind(pluginName); it != factories.constEnd()) { + return it.value(); + } + + // Even plugins that aren't found are in the map, so we know we shouldn't check again withthis expensive operation + factories[pluginName] = nullptr; + +#ifdef KIRIGAMI_BUILD_TYPE_STATIC + for (QObject *staticPlugin : QPluginLoader::staticInstances()) { + PlatformPluginFactory *factory = qobject_cast(staticPlugin); + if (factory) { + factories[pluginName] = factory; + break; + } + } +#else + const auto libraryPaths = QCoreApplication::libraryPaths(); + for (const QString &path : libraryPaths) { + +#ifdef Q_OS_ANDROID + const QDir dir(path); +#else + const QDir dir(path + QStringLiteral("/kf6/kirigami/platform")); +#endif + + const auto fileNames = dir.entryList(QDir::Files); + + for (const QString &fileName : fileNames) { + +#ifdef Q_OS_ANDROID + if (fileName.startsWith(QStringLiteral("libplugins_kf6_kirigami_platform_")) && QLibrary::isLibrary(fileName)) { +#endif + if (!pluginName.isEmpty() && fileName.contains(pluginName)) { + // TODO: env variable too? + QPluginLoader loader(dir.absoluteFilePath(fileName)); + QObject *plugin = loader.instance(); + // TODO: load actually a factory as plugin + + qCDebug(KirigamiPlatform) << "Loading style plugin from" << dir.absoluteFilePath(fileName); + + if (auto factory = qobject_cast(plugin)) { + factories[pluginName] = factory; + break; + } + } +#ifdef Q_OS_ANDROID + } +#endif + } + + // Ensure we only load the first plugin from the first plugin location. + // If we do not break here, we may end up loading a different plugin + // in place of the first one. + if (factories.value(pluginName) != nullptr) { + break; + } + } +#endif + + PlatformPluginFactory *factory = factories.value(pluginName); + + if (factory == nullptr) { + qWarning(KirigamiPlatform) << "Failed to find a Kirigami platform plugin for style" << QQuickStyle::name(); + } + + return factory; +} + +} +} + +#include "moc_platformpluginfactory.cpp" diff --git a/src/platform/platformpluginfactory.h b/src/platform/platformpluginfactory.h new file mode 100644 index 0000000..a598b86 --- /dev/null +++ b/src/platform/platformpluginfactory.h @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef KIRIGAMI_PLATFORMPLUGINFACTORY_H +#define KIRIGAMI_PLATFORMPLUGINFACTORY_H + +#include + +#include "kirigamiplatform_export.h" + +class QQmlEngine; + +namespace Kirigami +{ +namespace Platform +{ +class PlatformTheme; +class Units; + +/** + * @class PlatformPluginFactory platformpluginfactory.h + * + * This class is reimpleented by plugins to provide different implementations + * of PlatformTheme + */ +class KIRIGAMIPLATFORM_EXPORT PlatformPluginFactory : public QObject +{ + Q_OBJECT + +public: + explicit PlatformPluginFactory(QObject *parent = nullptr); + ~PlatformPluginFactory() override; + + /** + * Creates an instance of PlatformTheme which can come out from + * an implementation provided by a plugin + * + * If this returns nullptr the PlatformTheme will use a fallback + * implementation that loads a theme definition from a QML file. + * + * @param parent the parent object of the created PlatformTheme + */ + virtual PlatformTheme *createPlatformTheme(QObject *parent) = 0; + + /** + * Creates an instance of Units which can come from an implementation + * provided by a plugin + * @param parent the parent of the units object + */ + virtual Units *createUnits(QObject *parent) = 0; + + /** + * finds the plugin providing units and platformtheme for the current style + * The plugin pointer is cached, so only the first call is a potentially heavy operation + * @param pluginName The name we want to search for, if empty the name of + * the current QtQuickControls style will be searched + * @return pointer to the PlatformPluginFactory of the current style + */ + static PlatformPluginFactory *findPlugin(const QString &pluginName = {}); +}; + +} +} + +QT_BEGIN_NAMESPACE +#define PlatformPluginFactory_iid "org.kde.kirigami.PlatformPluginFactory" +Q_DECLARE_INTERFACE(Kirigami::Platform::PlatformPluginFactory, PlatformPluginFactory_iid) +QT_END_NAMESPACE + +#endif // PLATFORMPLUGINFACTORY_H diff --git a/src/platform/platformtheme.cpp b/src/platform/platformtheme.cpp new file mode 100644 index 0000000..af2ab92 --- /dev/null +++ b/src/platform/platformtheme.cpp @@ -0,0 +1,1092 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "platformtheme.h" +#include "basictheme_p.h" +#include "platformpluginfactory.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Kirigami +{ +namespace Platform +{ +template<> +KIRIGAMIPLATFORM_EXPORT QEvent::Type PlatformThemeEvents::DataChangedEvent::type = QEvent::None; +template<> +KIRIGAMIPLATFORM_EXPORT QEvent::Type PlatformThemeEvents::ColorSetChangedEvent::type = QEvent::None; +template<> +KIRIGAMIPLATFORM_EXPORT QEvent::Type PlatformThemeEvents::ColorGroupChangedEvent::type = QEvent::None; +template<> +KIRIGAMIPLATFORM_EXPORT QEvent::Type PlatformThemeEvents::ColorChangedEvent::type = QEvent::None; +template<> +KIRIGAMIPLATFORM_EXPORT QEvent::Type PlatformThemeEvents::FontChangedEvent::type = QEvent::None; + +// Initialize event types. +// We want to avoid collisions with application event types so we should use +// registerEventType for generating the event types. Unfortunately, that method +// is not constexpr so we need to call it somewhere during application startup. +// This struct handles that. +struct TypeInitializer { + TypeInitializer() + { + PlatformThemeEvents::DataChangedEvent::type = QEvent::Type(QEvent::registerEventType()); + PlatformThemeEvents::ColorSetChangedEvent::type = QEvent::Type(QEvent::registerEventType()); + PlatformThemeEvents::ColorGroupChangedEvent::type = QEvent::Type(QEvent::registerEventType()); + PlatformThemeEvents::ColorChangedEvent::type = QEvent::Type(QEvent::registerEventType()); + PlatformThemeEvents::FontChangedEvent::type = QEvent::Type(QEvent::registerEventType()); + } +}; +static TypeInitializer initializer; + +// This class encapsulates the actual data of the Theme object. It may be shared +// among several instances of PlatformTheme, to ensure that the memory usage of +// PlatformTheme stays low. +class PlatformThemeData : public QObject +{ + Q_OBJECT + +public: + // An enum for all colors in PlatformTheme. + // This is used so we can have a QHash of local overrides in the + // PlatformTheme, which avoids needing to store all these colors in + // PlatformTheme even when they're not used. + enum ColorRole { + TextColor, + DisabledTextColor, + HighlightedTextColor, + ActiveTextColor, + LinkColor, + VisitedLinkColor, + NegativeTextColor, + NeutralTextColor, + PositiveTextColor, + BackgroundColor, + AlternateBackgroundColor, + HighlightColor, + ActiveBackgroundColor, + LinkBackgroundColor, + VisitedLinkBackgroundColor, + NegativeBackgroundColor, + NeutralBackgroundColor, + PositiveBackgroundColor, + FocusColor, + HoverColor, + + // This should always be the last item. It indicates how many items + // there are and is used for the storage array below. + ColorRoleCount, + }; + + using ColorMap = std::unordered_map::type, QColor>; + + // Which PlatformTheme instance "owns" this data object. Only the owner is + // allowed to make changes to data. + QPointer owner; + + PlatformTheme::ColorSet colorSet = PlatformTheme::Window; + PlatformTheme::ColorGroup colorGroup = PlatformTheme::Active; + + std::array colors; + + QFont defaultFont; + QFont smallFont; + + QPalette palette; + + // A list of PlatformTheme instances that want to be notified when the data + // changes. This is used instead of signal/slots as this way we only store + // a little bit of data and that data is shared among instances, whereas + // signal/slots turn out to have a pretty large memory overhead per instance. + using Watcher = PlatformTheme *; + QList watchers; + + inline void setColorSet(PlatformTheme *sender, PlatformTheme::ColorSet set) + { + if (sender != owner || colorSet == set) { + return; + } + + auto oldValue = colorSet; + + colorSet = set; + + notifyWatchers(sender, oldValue, set); + } + + inline void setColorGroup(PlatformTheme *sender, PlatformTheme::ColorGroup group) + { + if (sender != owner || colorGroup == group) { + return; + } + + auto oldValue = colorGroup; + + colorGroup = group; + palette.setCurrentColorGroup(QPalette::ColorGroup(group)); + + notifyWatchers(sender, oldValue, group); + } + + inline void setColor(PlatformTheme *sender, ColorRole role, const QColor &color) + { + if (sender != owner || colors[role] == color) { + return; + } + + auto oldValue = colors[role]; + + colors[role] = color; + updatePalette(palette, colors); + + notifyWatchers(sender, oldValue, colors[role]); + } + + inline void setDefaultFont(PlatformTheme *sender, const QFont &font) + { + if (sender != owner || font == defaultFont) { + return; + } + + auto oldValue = defaultFont; + + defaultFont = font; + + notifyWatchers(sender, oldValue, font); + } + + inline void setSmallFont(PlatformTheme *sender, const QFont &font) + { + if (sender != owner || font == smallFont) { + return; + } + + auto oldValue = smallFont; + + smallFont = font; + + notifyWatchers(sender, oldValue, smallFont); + } + + inline void addChangeWatcher(PlatformTheme *object) + { + watchers.append(object); + } + + inline void removeChangeWatcher(PlatformTheme *object) + { + watchers.removeOne(object); + } + + template + inline void notifyWatchers(PlatformTheme *sender, const T &oldValue, const T &newValue) + { + for (auto object : std::as_const(watchers)) { + PlatformThemeEvents::PropertyChangedEvent event(sender, oldValue, newValue); + QCoreApplication::sendEvent(object, &event); + } + } + + // Update a palette from a list of colors. + inline static void updatePalette(QPalette &palette, const std::array &colors) + { + for (std::size_t i = 0; i < colors.size(); ++i) { + setPaletteColor(palette, ColorRole(i), colors.at(i)); + } + } + + // Update a palette from a hash of colors. + inline static void updatePalette(QPalette &palette, const ColorMap &colors) + { + for (auto entry : colors) { + setPaletteColor(palette, ColorRole(entry.first), entry.second); + } + } + + inline static void setPaletteColor(QPalette &palette, ColorRole role, const QColor &color) + { + switch (role) { + case TextColor: + palette.setColor(QPalette::Text, color); + palette.setColor(QPalette::WindowText, color); + palette.setColor(QPalette::ButtonText, color); + break; + case BackgroundColor: + palette.setColor(QPalette::Window, color); + palette.setColor(QPalette::Base, color); + palette.setColor(QPalette::Button, color); + break; + case AlternateBackgroundColor: + palette.setColor(QPalette::AlternateBase, color); + break; + case HighlightColor: + palette.setColor(QPalette::Highlight, color); + palette.setColor(QPalette::Accent, color); + break; + case HighlightedTextColor: + palette.setColor(QPalette::HighlightedText, color); + break; + case LinkColor: + palette.setColor(QPalette::Link, color); + break; + case VisitedLinkColor: + palette.setColor(QPalette::LinkVisited, color); + break; + + default: + break; + } + } +}; + +class PlatformThemePrivate +{ +public: + PlatformThemePrivate() + : inherit(true) + , supportsIconColoring(false) + , pendingColorChange(false) + , pendingChildUpdate(false) + , useAlternateBackgroundColor(false) + , colorSet(PlatformTheme::Window) + , colorGroup(PlatformTheme::Active) + { + } + + inline QColor color(const PlatformTheme *theme, PlatformThemeData::ColorRole color) const + { + if (!data) { + return QColor{}; + } + + QColor value = data->colors.at(color); + + if (data->owner != theme && localOverrides) { + auto itr = localOverrides->find(color); + if (itr != localOverrides->end()) { + value = itr->second; + } + } + + return value; + } + + inline void setColor(PlatformTheme *theme, PlatformThemeData::ColorRole color, const QColor &value) + { + if (!localOverrides) { + localOverrides = std::make_unique(); + } + + if (!value.isValid()) { + // Invalid color, assume we are resetting the value. + auto itr = localOverrides->find(color); + if (itr != localOverrides->end()) { + PlatformThemeChangeTracker tracker(theme, PlatformThemeChangeTracker::PropertyChange::Color); + localOverrides->erase(itr); + + if (data) { + // TODO: Find a better way to determine "default" color. + // Right now this sets the color to transparent to force a + // color change and relies on the style-specific subclass to + // handle resetting the actual color. + data->setColor(theme, color, Qt::transparent); + } + } + + return; + } + + auto itr = localOverrides->find(color); + if (itr != localOverrides->end() && itr->second == value && (data && data->owner != theme)) { + return; + } + + PlatformThemeChangeTracker tracker(theme, PlatformThemeChangeTracker::PropertyChange::Color); + + (*localOverrides)[color] = value; + + if (data) { + data->setColor(theme, color, value); + } + } + + inline void setDataColor(PlatformTheme *theme, PlatformThemeData::ColorRole color, const QColor &value) + { + // Only set color if we have no local override of the color. + // This is done because colorSet/colorGroup changes will trigger most + // subclasses to reevaluate and reset the colors, breaking any local + // overrides we have. + if (localOverrides) { + auto itr = localOverrides->find(color); + if (itr != localOverrides->end()) { + return; + } + } + + PlatformThemeChangeTracker tracker(theme, PlatformThemeChangeTracker::PropertyChange::Color); + + if (data) { + data->setColor(theme, color, value); + } + } + + /* + * Please note that there is no q pointer. This is intentional, as it avoids + * having to store that information for each instance of PlatformTheme, + * saving us 8 bytes per instance. Instead, we pass the theme object as + * first parameter of each method. This is a little uglier but essentially + * works the same without needing memory. + */ + + // An instance of the data object. This is potentially shared with many + // instances of PlatformTheme. + std::shared_ptr data; + // Used to store color overrides of inherited data. This is created on + // demand and will only exist if we actually have local overrides. + std::unique_ptr localOverrides; + + bool inherit : 1; + bool supportsIconColoring : 1; // TODO KF6: Remove in favour of virtual method + bool pendingColorChange : 1; + bool pendingChildUpdate : 1; + bool useAlternateBackgroundColor : 1; + + // Note: We use these to store local values of PlatformTheme::ColorSet and + // PlatformTheme::ColorGroup. While these are standard enums and thus 32 + // bits they only contain a few items so we store the value in only 4 bits + // to save space. + uint8_t colorSet : 4; + uint8_t colorGroup : 4; + + // Ensure the above assumption holds. Should this static assert fail, the + // bit size above needs to be adjusted. + static_assert(PlatformTheme::ColorGroupCount <= 16, "PlatformTheme::ColorGroup contains more elements than can be stored in PlatformThemePrivate"); + static_assert(PlatformTheme::ColorSetCount <= 16, "PlatformTheme::ColorSet contains more elements than can be stored in PlatformThemePrivate"); + + inline static PlatformPluginFactory *s_pluginFactory = nullptr; +}; + +PlatformTheme::PlatformTheme(QObject *parent) + : QObject(parent) + , d(new PlatformThemePrivate) +{ + if (QQuickItem *item = qobject_cast(parent)) { + connect(item, &QQuickItem::windowChanged, this, [this](QQuickWindow *window) { + if (window) { + update(); + } + }); + connect(item, &QQuickItem::parentChanged, this, &PlatformTheme::update); + // Needs to be connected to enabledChanged twice to correctly fully update when a + // Theme that does inherit becomes temporarly non-inherit and back due to + // the item being enabled or disabled + connect(item, &QQuickItem::enabledChanged, this, &PlatformTheme::update); + connect(item, &QQuickItem::enabledChanged, this, &PlatformTheme::update, Qt::QueuedConnection); + } + + update(); +} + +PlatformTheme::~PlatformTheme() +{ + if (d->data) { + d->data->removeChangeWatcher(this); + } + + delete d; +} + +void PlatformTheme::setColorSet(PlatformTheme::ColorSet colorSet) +{ + PlatformThemeChangeTracker tracker(this, PlatformThemeChangeTracker::PropertyChange::ColorSet); + d->colorSet = colorSet; + + if (d->data) { + d->data->setColorSet(this, colorSet); + } +} + +PlatformTheme::ColorSet PlatformTheme::colorSet() const +{ + return d->data ? d->data->colorSet : Window; +} + +void PlatformTheme::setColorGroup(PlatformTheme::ColorGroup colorGroup) +{ + PlatformThemeChangeTracker tracker(this, PlatformThemeChangeTracker::PropertyChange::ColorGroup); + d->colorGroup = colorGroup; + + if (d->data) { + d->data->setColorGroup(this, colorGroup); + } +} + +PlatformTheme::ColorGroup PlatformTheme::colorGroup() const +{ + return d->data ? d->data->colorGroup : Active; +} + +bool PlatformTheme::inherit() const +{ + return d->inherit; +} + +void PlatformTheme::setInherit(bool inherit) +{ + if (inherit == d->inherit) { + return; + } + + d->inherit = inherit; + update(); + + Q_EMIT inheritChanged(inherit); +} + +QColor PlatformTheme::textColor() const +{ + return d->color(this, PlatformThemeData::TextColor); +} + +QColor PlatformTheme::disabledTextColor() const +{ + return d->color(this, PlatformThemeData::DisabledTextColor); +} + +QColor PlatformTheme::highlightColor() const +{ + return d->color(this, PlatformThemeData::HighlightColor); +} + +QColor PlatformTheme::highlightedTextColor() const +{ + return d->color(this, PlatformThemeData::HighlightedTextColor); +} + +QColor PlatformTheme::backgroundColor() const +{ + return d->color(this, PlatformThemeData::BackgroundColor); +} + +QColor PlatformTheme::alternateBackgroundColor() const +{ + return d->color(this, PlatformThemeData::AlternateBackgroundColor); +} + +QColor PlatformTheme::activeTextColor() const +{ + return d->color(this, PlatformThemeData::ActiveTextColor); +} + +QColor PlatformTheme::activeBackgroundColor() const +{ + return d->color(this, PlatformThemeData::ActiveBackgroundColor); +} + +QColor PlatformTheme::linkColor() const +{ + return d->color(this, PlatformThemeData::LinkColor); +} + +QColor PlatformTheme::linkBackgroundColor() const +{ + return d->color(this, PlatformThemeData::LinkBackgroundColor); +} + +QColor PlatformTheme::visitedLinkColor() const +{ + return d->color(this, PlatformThemeData::VisitedLinkColor); +} + +QColor PlatformTheme::visitedLinkBackgroundColor() const +{ + return d->color(this, PlatformThemeData::VisitedLinkBackgroundColor); +} + +QColor PlatformTheme::negativeTextColor() const +{ + return d->color(this, PlatformThemeData::NegativeTextColor); +} + +QColor PlatformTheme::negativeBackgroundColor() const +{ + return d->color(this, PlatformThemeData::NegativeBackgroundColor); +} + +QColor PlatformTheme::neutralTextColor() const +{ + return d->color(this, PlatformThemeData::NeutralTextColor); +} + +QColor PlatformTheme::neutralBackgroundColor() const +{ + return d->color(this, PlatformThemeData::NeutralBackgroundColor); +} + +QColor PlatformTheme::positiveTextColor() const +{ + return d->color(this, PlatformThemeData::PositiveTextColor); +} + +QColor PlatformTheme::positiveBackgroundColor() const +{ + return d->color(this, PlatformThemeData::PositiveBackgroundColor); +} + +QColor PlatformTheme::focusColor() const +{ + return d->color(this, PlatformThemeData::FocusColor); +} + +QColor PlatformTheme::hoverColor() const +{ + return d->color(this, PlatformThemeData::HoverColor); +} + +// setters for theme implementations +void PlatformTheme::setTextColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::TextColor, color); +} + +void PlatformTheme::setDisabledTextColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::DisabledTextColor, color); +} + +void PlatformTheme::setBackgroundColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::BackgroundColor, color); +} + +void PlatformTheme::setAlternateBackgroundColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::AlternateBackgroundColor, color); +} + +void PlatformTheme::setHighlightColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::HighlightColor, color); +} + +void PlatformTheme::setHighlightedTextColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::HighlightedTextColor, color); +} + +void PlatformTheme::setActiveTextColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::ActiveTextColor, color); +} + +void PlatformTheme::setActiveBackgroundColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::ActiveBackgroundColor, color); +} + +void PlatformTheme::setLinkColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::LinkColor, color); +} + +void PlatformTheme::setLinkBackgroundColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::LinkBackgroundColor, color); +} + +void PlatformTheme::setVisitedLinkColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::VisitedLinkColor, color); +} + +void PlatformTheme::setVisitedLinkBackgroundColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::VisitedLinkBackgroundColor, color); +} + +void PlatformTheme::setNegativeTextColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::NegativeTextColor, color); +} + +void PlatformTheme::setNegativeBackgroundColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::NegativeBackgroundColor, color); +} + +void PlatformTheme::setNeutralTextColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::NeutralTextColor, color); +} + +void PlatformTheme::setNeutralBackgroundColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::NeutralBackgroundColor, color); +} + +void PlatformTheme::setPositiveTextColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::PositiveTextColor, color); +} + +void PlatformTheme::setPositiveBackgroundColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::PositiveBackgroundColor, color); +} + +void PlatformTheme::setHoverColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::HoverColor, color); +} + +void PlatformTheme::setFocusColor(const QColor &color) +{ + d->setDataColor(this, PlatformThemeData::FocusColor, color); +} + +QFont PlatformTheme::defaultFont() const +{ + return d->data ? d->data->defaultFont : QFont{}; +} + +void PlatformTheme::setDefaultFont(const QFont &font) +{ + PlatformThemeChangeTracker tracker(this, PlatformThemeChangeTracker::PropertyChange::Font); + if (d->data) { + d->data->setDefaultFont(this, font); + } +} + +QFont PlatformTheme::smallFont() const +{ + return d->data ? d->data->smallFont : QFont{}; +} + +void PlatformTheme::setSmallFont(const QFont &font) +{ + PlatformThemeChangeTracker tracker(this, PlatformThemeChangeTracker::PropertyChange::Font); + if (d->data) { + d->data->setSmallFont(this, font); + } +} + +qreal PlatformTheme::frameContrast() const +{ + // This value must be kept in sync with + // the value from Breeze Qt Widget theme. + // See: https://invent.kde.org/plasma/breeze/-/blob/master/kstyle/breezemetrics.h?ref_type=heads#L162 + return 0.20; +} + +qreal PlatformTheme::lightFrameContrast() const +{ + // This can be utilized to return full contrast + // if high contrast accessibility setting is enabled + return frameContrast() / 2.0; +} + +// setters for QML clients +void PlatformTheme::setCustomTextColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::TextColor, color); +} + +void PlatformTheme::setCustomDisabledTextColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::DisabledTextColor, color); +} + +void PlatformTheme::setCustomBackgroundColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::BackgroundColor, color); +} + +void PlatformTheme::setCustomAlternateBackgroundColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::AlternateBackgroundColor, color); +} + +void PlatformTheme::setCustomHighlightColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::HighlightColor, color); +} + +void PlatformTheme::setCustomHighlightedTextColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::HighlightedTextColor, color); +} + +void PlatformTheme::setCustomActiveTextColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::ActiveTextColor, color); +} + +void PlatformTheme::setCustomActiveBackgroundColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::ActiveBackgroundColor, color); +} + +void PlatformTheme::setCustomLinkColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::LinkColor, color); +} + +void PlatformTheme::setCustomLinkBackgroundColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::LinkBackgroundColor, color); +} + +void PlatformTheme::setCustomVisitedLinkColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::TextColor, color); +} + +void PlatformTheme::setCustomVisitedLinkBackgroundColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::VisitedLinkBackgroundColor, color); +} + +void PlatformTheme::setCustomNegativeTextColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::NegativeTextColor, color); +} + +void PlatformTheme::setCustomNegativeBackgroundColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::NegativeBackgroundColor, color); +} + +void PlatformTheme::setCustomNeutralTextColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::NeutralTextColor, color); +} + +void PlatformTheme::setCustomNeutralBackgroundColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::NeutralBackgroundColor, color); +} + +void PlatformTheme::setCustomPositiveTextColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::PositiveTextColor, color); +} + +void PlatformTheme::setCustomPositiveBackgroundColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::PositiveBackgroundColor, color); +} + +void PlatformTheme::setCustomHoverColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::HoverColor, color); +} + +void PlatformTheme::setCustomFocusColor(const QColor &color) +{ + d->setColor(this, PlatformThemeData::FocusColor, color); +} + +bool PlatformTheme::useAlternateBackgroundColor() const +{ + return d->useAlternateBackgroundColor; +} + +void PlatformTheme::setUseAlternateBackgroundColor(bool alternate) +{ + if (alternate == d->useAlternateBackgroundColor) { + return; + } + + d->useAlternateBackgroundColor = alternate; + Q_EMIT useAlternateBackgroundColorChanged(alternate); +} + +QPalette PlatformTheme::palette() const +{ + if (!d->data) { + return QPalette{}; + } + + auto palette = d->data->palette; + + if (d->localOverrides) { + PlatformThemeData::updatePalette(palette, *d->localOverrides); + } + + return palette; +} + +QIcon PlatformTheme::iconFromTheme(const QString &name, const QColor &customColor) +{ + Q_UNUSED(customColor); + QIcon icon = QIcon::fromTheme(name); + return icon; +} + +bool PlatformTheme::supportsIconColoring() const +{ + return d->supportsIconColoring; +} + +void PlatformTheme::setSupportsIconColoring(bool support) +{ + d->supportsIconColoring = support; +} + +PlatformTheme *PlatformTheme::qmlAttachedProperties(QObject *object) +{ + QQmlEngine *engine = qmlEngine(object); + QString pluginName; + + if (engine) { + pluginName = engine->property("_kirigamiTheme").toString(); + } + + auto plugin = PlatformPluginFactory::findPlugin(pluginName); + if (!plugin && !pluginName.isEmpty()) { + plugin = PlatformPluginFactory::findPlugin(); + } + + if (plugin) { + if (auto theme = plugin->createPlatformTheme(object)) { + return theme; + } + } + + return new BasicTheme(object); +} + +void PlatformTheme::emitSignalsForChanges(int changes) +{ + if (!d->data) { + return; + } + + auto propertyChanges = PlatformThemeChangeTracker::PropertyChanges::fromInt(changes); + + if (propertyChanges & PlatformThemeChangeTracker::PropertyChange::ColorSet) { + Q_EMIT colorSetChanged(ColorSet(d->data->colorSet)); + } + + if (propertyChanges & PlatformThemeChangeTracker::PropertyChange::ColorGroup) { + Q_EMIT colorGroupChanged(ColorGroup(d->data->colorGroup)); + } + + if (propertyChanges & PlatformThemeChangeTracker::PropertyChange::Color) { + Q_EMIT colorsChanged(); + } + + if (propertyChanges & PlatformThemeChangeTracker::PropertyChange::Palette) { + Q_EMIT paletteChanged(d->data->palette); + } + + if (propertyChanges & PlatformThemeChangeTracker::PropertyChange::Font) { + Q_EMIT defaultFontChanged(d->data->defaultFont); + Q_EMIT smallFontChanged(d->data->smallFont); + } + + if (propertyChanges & PlatformThemeChangeTracker::PropertyChange::Data) { + updateChildren(parent()); + } +} + +bool PlatformTheme::event(QEvent *event) +{ + PlatformThemeChangeTracker tracker(this); + + if (event->type() == PlatformThemeEvents::DataChangedEvent::type) { + auto changeEvent = static_cast(event); + + if (changeEvent->sender != this) { + return false; + } + + if (changeEvent->oldValue) { + changeEvent->oldValue->removeChangeWatcher(this); + } + + if (changeEvent->newValue) { + auto data = changeEvent->newValue; + data->addChangeWatcher(this); + } + + tracker.markDirty(PlatformThemeChangeTracker::PropertyChange::All); + return true; + } + + if (event->type() == PlatformThemeEvents::ColorSetChangedEvent::type) { + tracker.markDirty(PlatformThemeChangeTracker::PropertyChange::ColorSet); + return true; + } + + if (event->type() == PlatformThemeEvents::ColorGroupChangedEvent::type) { + tracker.markDirty(PlatformThemeChangeTracker::PropertyChange::ColorGroup); + return true; + } + + if (event->type() == PlatformThemeEvents::ColorChangedEvent::type) { + tracker.markDirty(PlatformThemeChangeTracker::PropertyChange::Color | PlatformThemeChangeTracker::PropertyChange::Palette); + return true; + } + + if (event->type() == PlatformThemeEvents::FontChangedEvent::type) { + tracker.markDirty(PlatformThemeChangeTracker::PropertyChange::Font); + return true; + } + + return QObject::event(event); +} + +void PlatformTheme::update() +{ + auto oldData = d->data; + + bool actualInherit = d->inherit; + if (QQuickItem *item = qobject_cast(parent())) { + // For inactive windows it should work already, as also the non inherit themes get it + if (colorGroup() != Disabled && !item->isEnabled()) { + actualInherit = false; + } + } + + if (actualInherit) { + QObject *candidate = parent(); + while (true) { + candidate = determineParent(candidate); + if (!candidate) { + break; + } + + auto t = static_cast(qmlAttachedPropertiesObject(candidate, false)); + if (t && t->d->data && t->d->data->owner == t) { + if (d->data == t->d->data) { + // Inheritance is already correct, do nothing. + return; + } + + d->data = t->d->data; + + PlatformThemeEvents::DataChangedEvent event{this, oldData, t->d->data}; + QCoreApplication::sendEvent(this, &event); + + return; + } + } + } else if (d->data && d->data->owner != this) { + // Inherit has changed and we no longer want to inherit, clear the data + // so it is recreated below. + d->data = nullptr; + } + + if (!d->data) { + d->data = std::make_shared(); + d->data->owner = this; + + d->data->setColorSet(this, static_cast(d->colorSet)); + d->data->setColorGroup(this, static_cast(d->colorGroup)); + + // If we normally inherit but do not do so currently due to an override, + // copy over the old colorSet to ensure we do not suddenly change to a + // different colorSet. + if (d->inherit && !actualInherit && oldData) { + d->data->setColorSet(this, oldData->colorSet); + } + } + + if (d->localOverrides) { + for (auto entry : *d->localOverrides) { + d->data->setColor(this, PlatformThemeData::ColorRole(entry.first), entry.second); + } + } + + PlatformThemeEvents::DataChangedEvent event{this, oldData, d->data}; + QCoreApplication::sendEvent(this, &event); +} + +void PlatformTheme::updateChildren(QObject *object) +{ + if (!object) { + return; + } + + const auto children = object->children(); + for (auto child : children) { + auto t = static_cast(qmlAttachedPropertiesObject(child, false)); + if (t) { + t->update(); + } else { + updateChildren(child); + } + } +} + +// We sometimes set theme properties on non-visual objects. However, if an item +// has a visual and a non-visual parent that are different, we should prefer the +// visual parent, so we need to apply some extra logic. +QObject *PlatformTheme::determineParent(QObject *object) +{ + if (!object) { + return nullptr; + } + + auto item = qobject_cast(object); + if (item) { + return item->parentItem(); + } else { + return object->parent(); + } +} + +PlatformThemeChangeTracker::PlatformThemeChangeTracker(PlatformTheme *theme, PropertyChanges changes) + : m_theme(theme) +{ + auto itr = s_blockedChanges.constFind(theme); + if (itr == s_blockedChanges.constEnd() || (*itr).expired()) { + m_data = std::make_shared(); + s_blockedChanges.insert(theme, m_data); + } else { + m_data = (*itr).lock(); + } + + m_data->changes |= changes; +} + +PlatformThemeChangeTracker::~PlatformThemeChangeTracker() noexcept +{ + std::weak_ptr dataWatcher = m_data; + + auto changes = m_data->changes; + m_data.reset(); + + if (dataWatcher.use_count() <= 0) { + m_theme->emitSignalsForChanges(changes); + s_blockedChanges.remove(m_theme); + } +} + +void PlatformThemeChangeTracker::markDirty(PropertyChanges changes) +{ + m_data->changes |= changes; +} +} +} + +#include "moc_platformtheme.cpp" +#include "platformtheme.moc" diff --git a/src/platform/platformtheme.h b/src/platform/platformtheme.h new file mode 100644 index 0000000..562c6f8 --- /dev/null +++ b/src/platform/platformtheme.h @@ -0,0 +1,470 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef KIRIGAMI_PLATFORMTHEME_H +#define KIRIGAMI_PLATFORMTHEME_H + +#include +#include +#include +#include +#include +#include + +#include "kirigamiplatform_export.h" + +namespace Kirigami +{ +namespace Platform +{ +class PlatformThemeData; +class PlatformThemePrivate; + +/** + * @class PlatformTheme platformtheme.h + * + * This class is the base for color management in Kirigami, + * different platforms can reimplement this class to integrate with + * system platform colors of a given platform + */ +class KIRIGAMIPLATFORM_EXPORT PlatformTheme : public QObject +{ + Q_OBJECT + QML_NAMED_ELEMENT(Theme) + QML_ATTACHED(Kirigami::Platform::PlatformTheme) + QML_UNCREATABLE("Attached Property") + + /** + * This enumeration describes the color set for which a color is being selected. + * + * Color sets define a color "environment", suitable for drawing all parts of a + * given region. Colors from different sets should not be combined. + */ + Q_PROPERTY(ColorSet colorSet READ colorSet WRITE setColorSet NOTIFY colorSetChanged FINAL) + + /** + * This enumeration describes the color group used to generate the colors. + * The enum value is based upon QPalette::ColorGroup and has the same values. + * It's redefined here in order to make it work with QML. + * @since 4.43 + */ + Q_PROPERTY(ColorGroup colorGroup READ colorGroup WRITE setColorGroup NOTIFY colorGroupChanged FINAL) + + /** + * If true, the colorSet will be inherited from the colorset of a theme of one + * of the ancestor items + * default: true + */ + Q_PROPERTY(bool inherit READ inherit WRITE setInherit NOTIFY inheritChanged FINAL) + + // foreground colors + /** + * Color for normal foregrounds, usually text, but not limited to it, + * anything that should be painted with a clear contrast should use this color + */ + Q_PROPERTY(QColor textColor READ textColor WRITE setCustomTextColor RESET setCustomTextColor NOTIFY colorsChanged FINAL) + + /** + * Foreground color for disabled areas, usually a mid-gray + * @note Depending on the implementation, the color used for this property may not be + * based on the disabled palette. For example, for the Plasma implementation, + * "Inactive Text Color" of the active palette is used. + */ + Q_PROPERTY(QColor disabledTextColor READ disabledTextColor WRITE setCustomDisabledTextColor RESET setCustomDisabledTextColor NOTIFY colorsChanged FINAL) + + /** + * Color for text that has been highlighted, often is a light color while normal text is dark + */ + Q_PROPERTY( + QColor highlightedTextColor READ highlightedTextColor WRITE setCustomHighlightedTextColor RESET setCustomHighlightedTextColor NOTIFY colorsChanged) + + /** + * Foreground for areas that are active or requesting attention + */ + Q_PROPERTY(QColor activeTextColor READ activeTextColor WRITE setCustomActiveTextColor RESET setCustomActiveTextColor NOTIFY colorsChanged FINAL) + + /** + * Color for links + */ + Q_PROPERTY(QColor linkColor READ linkColor WRITE setCustomLinkColor RESET setCustomLinkColor NOTIFY colorsChanged FINAL) + + /** + * Color for visited links, usually a bit darker than linkColor + */ + Q_PROPERTY(QColor visitedLinkColor READ visitedLinkColor WRITE setCustomVisitedLinkColor RESET setCustomVisitedLinkColor NOTIFY colorsChanged FINAL) + + /** + * Foreground color for negative areas, such as critical error text + */ + Q_PROPERTY(QColor negativeTextColor READ negativeTextColor WRITE setCustomNegativeTextColor RESET setCustomNegativeTextColor NOTIFY colorsChanged FINAL) + + /** + * Foreground color for neutral areas, such as warning texts (but not critical) + */ + Q_PROPERTY(QColor neutralTextColor READ neutralTextColor WRITE setCustomNeutralTextColor RESET setCustomNeutralTextColor NOTIFY colorsChanged FINAL) + + /** + * Success messages, trusted content + */ + Q_PROPERTY(QColor positiveTextColor READ positiveTextColor WRITE setCustomPositiveTextColor RESET setCustomPositiveTextColor NOTIFY colorsChanged FINAL) + + // background colors + /** + * The generic background color + */ + Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setCustomBackgroundColor RESET setCustomBackgroundColor NOTIFY colorsChanged FINAL) + + /** + * The generic background color + * Alternate background; for example, for use in lists. + * This color may be the same as BackgroundNormal, + * especially in sets other than View and Window. + */ + Q_PROPERTY(QColor alternateBackgroundColor READ alternateBackgroundColor WRITE setCustomAlternateBackgroundColor RESET setCustomAlternateBackgroundColor + NOTIFY colorsChanged) + + /** + * The background color for selected areas + */ + Q_PROPERTY(QColor highlightColor READ highlightColor WRITE setCustomHighlightColor RESET setCustomHighlightColor NOTIFY colorsChanged FINAL) + + /** + * Background for areas that are active or requesting attention + */ + Q_PROPERTY( + QColor activeBackgroundColor READ activeBackgroundColor WRITE setCustomActiveBackgroundColor RESET setCustomActiveBackgroundColor NOTIFY colorsChanged) + + /** + * Background color for links + */ + Q_PROPERTY( + QColor linkBackgroundColor READ linkBackgroundColor WRITE setCustomLinkBackgroundColor RESET setCustomLinkBackgroundColor NOTIFY colorsChanged FINAL) + + /** + * Background color for visited links, usually a bit darker than linkBackgroundColor + */ + Q_PROPERTY(QColor visitedLinkBackgroundColor READ visitedLinkBackgroundColor WRITE setCustomVisitedLinkBackgroundColor RESET + setCustomVisitedLinkBackgroundColor NOTIFY colorsChanged) + + /** + * Background color for negative areas, such as critical errors and destructive actions + */ + Q_PROPERTY(QColor negativeBackgroundColor READ negativeBackgroundColor WRITE setCustomNegativeBackgroundColor RESET setCustomNegativeBackgroundColor NOTIFY + colorsChanged) + + /** + * Background color for neutral areas, such as warnings (but not critical) + */ + Q_PROPERTY(QColor neutralBackgroundColor READ neutralBackgroundColor WRITE setCustomNeutralBackgroundColor RESET setCustomNeutralBackgroundColor NOTIFY + colorsChanged) + + /** + * Background color for positive areas, such as success messages and trusted content + */ + Q_PROPERTY(QColor positiveBackgroundColor READ positiveBackgroundColor WRITE setCustomPositiveBackgroundColor RESET setCustomPositiveBackgroundColor NOTIFY + colorsChanged) + + // decoration colors + /** + * A decoration color that indicates active focus + */ + Q_PROPERTY(QColor focusColor READ focusColor WRITE setCustomFocusColor RESET setCustomFocusColor NOTIFY colorsChanged FINAL) + + /** + * A decoration color that indicates mouse hovering + */ + Q_PROPERTY(QColor hoverColor READ hoverColor WRITE setCustomHoverColor RESET setCustomHoverColor NOTIFY colorsChanged FINAL) + + /** + * Hint for item views to actually make use of the alternate background color feature + */ + Q_PROPERTY( + bool useAlternateBackgroundColor READ useAlternateBackgroundColor WRITE setUseAlternateBackgroundColor NOTIFY useAlternateBackgroundColorChanged FINAL) + + // font and palette + Q_PROPERTY(QFont defaultFont READ defaultFont NOTIFY defaultFontChanged FINAL) + + // small font + Q_PROPERTY(QFont smallFont READ smallFont NOTIFY smallFontChanged FINAL) + + // Active palette + Q_PROPERTY(QPalette palette READ palette NOTIFY paletteChanged FINAL) + + // Frame contrast value, usually used for separators and outlines + // Value is between 0.0 and 1.0 + Q_PROPERTY(qreal frameContrast READ frameContrast CONSTANT FINAL) + + // Returns half of the frameContrast value; used by Separator.Weight.Light + // Value is between 0.0 and 1.0 + Q_PROPERTY(qreal lightFrameContrast READ lightFrameContrast CONSTANT FINAL) + +public: + enum ColorSet { + /** Color set for item views, usually the lightest of all */ + View = 0, + /** Default Color set for windows and "chrome" areas */ + Window, + /** Color set used by buttons */ + Button, + /** Color set used by selected areas */ + Selection, + /** Color set used by tooltips */ + Tooltip, + /** Color set meant to be complementary to Window: usually is a dark theme for light themes */ + Complementary, + /** Color set to be used by heading areas of applications, such as toolbars */ + Header, + // Number of items in this enum, this should always be the last item. + ColorSetCount, + }; + Q_ENUM(ColorSet) + + enum ColorGroup { + Disabled = QPalette::Disabled, + Active = QPalette::Active, + Inactive = QPalette::Inactive, + Normal = QPalette::Normal, + + ColorGroupCount, // Number of items in this enum, this should always be the last item. + }; + Q_ENUM(ColorGroup) + + explicit PlatformTheme(QObject *parent = nullptr); + ~PlatformTheme() override; + + void setColorSet(PlatformTheme::ColorSet); + PlatformTheme::ColorSet colorSet() const; + + void setColorGroup(PlatformTheme::ColorGroup); + PlatformTheme::ColorGroup colorGroup() const; + + bool inherit() const; + void setInherit(bool inherit); + + // foreground colors + QColor textColor() const; + QColor disabledTextColor() const; + QColor highlightedTextColor() const; + QColor activeTextColor() const; + QColor linkColor() const; + QColor visitedLinkColor() const; + QColor negativeTextColor() const; + QColor neutralTextColor() const; + QColor positiveTextColor() const; + + // background colors + QColor backgroundColor() const; + QColor alternateBackgroundColor() const; + QColor highlightColor() const; + QColor activeBackgroundColor() const; + QColor linkBackgroundColor() const; + QColor visitedLinkBackgroundColor() const; + QColor negativeBackgroundColor() const; + QColor neutralBackgroundColor() const; + QColor positiveBackgroundColor() const; + + // decoration colors + QColor focusColor() const; + QColor hoverColor() const; + + QFont defaultFont() const; + QFont smallFont() const; + + // this may is used by the desktop QQC2 to set the styleoption palettes + QPalette palette() const; + + qreal frameContrast() const; + qreal lightFrameContrast() const; + + // this will be used by desktopicon to fetch icons with KIconLoader + virtual Q_INVOKABLE QIcon iconFromTheme(const QString &name, const QColor &customColor = Qt::transparent); + + bool supportsIconColoring() const; + + // foreground colors + void setCustomTextColor(const QColor &color = QColor()); + void setCustomDisabledTextColor(const QColor &color = QColor()); + void setCustomHighlightedTextColor(const QColor &color = QColor()); + void setCustomActiveTextColor(const QColor &color = QColor()); + void setCustomLinkColor(const QColor &color = QColor()); + void setCustomVisitedLinkColor(const QColor &color = QColor()); + void setCustomNegativeTextColor(const QColor &color = QColor()); + void setCustomNeutralTextColor(const QColor &color = QColor()); + void setCustomPositiveTextColor(const QColor &color = QColor()); + // background colors + void setCustomBackgroundColor(const QColor &color = QColor()); + void setCustomAlternateBackgroundColor(const QColor &color = QColor()); + void setCustomHighlightColor(const QColor &color = QColor()); + void setCustomActiveBackgroundColor(const QColor &color = QColor()); + void setCustomLinkBackgroundColor(const QColor &color = QColor()); + void setCustomVisitedLinkBackgroundColor(const QColor &color = QColor()); + void setCustomNegativeBackgroundColor(const QColor &color = QColor()); + void setCustomNeutralBackgroundColor(const QColor &color = QColor()); + void setCustomPositiveBackgroundColor(const QColor &color = QColor()); + // decoration colors + void setCustomFocusColor(const QColor &color = QColor()); + void setCustomHoverColor(const QColor &color = QColor()); + + bool useAlternateBackgroundColor() const; + void setUseAlternateBackgroundColor(bool alternate); + + // QML attached property + static PlatformTheme *qmlAttachedProperties(QObject *object); + +Q_SIGNALS: + void colorsChanged(); + void defaultFontChanged(const QFont &font); + void smallFontChanged(const QFont &font); + void colorSetChanged(Kirigami::Platform::PlatformTheme::ColorSet colorSet); + void colorGroupChanged(Kirigami::Platform::PlatformTheme::ColorGroup colorGroup); + void paletteChanged(const QPalette &pal); + void inheritChanged(bool inherit); + void useAlternateBackgroundColorChanged(bool alternate); + +protected: + // Setters, not accessible from QML but from implementations + void setSupportsIconColoring(bool support); + + // foreground colors + void setTextColor(const QColor &color); + void setDisabledTextColor(const QColor &color); + void setHighlightedTextColor(const QColor &color); + void setActiveTextColor(const QColor &color); + void setLinkColor(const QColor &color); + void setVisitedLinkColor(const QColor &color); + void setNegativeTextColor(const QColor &color); + void setNeutralTextColor(const QColor &color); + void setPositiveTextColor(const QColor &color); + + // background colors + void setBackgroundColor(const QColor &color); + void setAlternateBackgroundColor(const QColor &color); + void setHighlightColor(const QColor &color); + void setActiveBackgroundColor(const QColor &color); + void setLinkBackgroundColor(const QColor &color); + void setVisitedLinkBackgroundColor(const QColor &color); + void setNegativeBackgroundColor(const QColor &color); + void setNeutralBackgroundColor(const QColor &color); + void setPositiveBackgroundColor(const QColor &color); + + // decoration colors + void setFocusColor(const QColor &color); + void setHoverColor(const QColor &color); + + void setDefaultFont(const QFont &defaultFont); + void setSmallFont(const QFont &smallFont); + + bool event(QEvent *event) override; + +private: + KIRIGAMIPLATFORM_NO_EXPORT void update(); + KIRIGAMIPLATFORM_NO_EXPORT void updateChildren(QObject *item); + KIRIGAMIPLATFORM_NO_EXPORT QObject *determineParent(QObject *object); + KIRIGAMIPLATFORM_NO_EXPORT void emitSignalsForChanges(int changes); + + PlatformThemePrivate *d; + friend class PlatformThemePrivate; + friend class PlatformThemeData; + friend class PlatformThemeChangeTracker; +}; + +/** + * A class that tracks changes to PlatformTheme properties and emits signals at the right moment. + * + * This should be used by PlatformTheme implementations to ensure that multiple + * changes to a PlatformTheme's properties do not emit multiple change signals, + * instead batching all of them into a single signal emission. This then ensures + * things making use of PlatformTheme aren't needlessly redrawn or redrawn in a + * partially changed state. + * + * @since 6.7 + * + */ +class KIRIGAMIPLATFORM_EXPORT PlatformThemeChangeTracker +{ +public: + /** + * Flags used to indicate changes made to certain properties. + */ + enum class PropertyChange : uint8_t { + None = 0, + ColorSet = 1 << 0, + ColorGroup = 1 << 1, + Color = 1 << 2, + Palette = 1 << 3, + Font = 1 << 4, + Data = 1 << 5, + All = ColorSet | ColorGroup | Color | Palette | Font | Data, + }; + Q_DECLARE_FLAGS(PropertyChanges, PropertyChange) + + PlatformThemeChangeTracker(PlatformTheme *theme, PropertyChanges changes = PropertyChange::None); + ~PlatformThemeChangeTracker(); + + void markDirty(PropertyChanges changes); + +private: + PlatformTheme *m_theme; + + // Per-PlatformTheme data that we need for PlatformThemeChangeBlocker. + // We don't want to store this in PlatformTheme since that would increase the + // size of every instance of PlatformTheme while it's only used when we want to + // block property change signal emissions. So instead we store it in a separate + // hash using the PlatformTheme as key. + struct Data { + PropertyChanges changes; + }; + + std::shared_ptr m_data; + + inline static QHash> s_blockedChanges; +}; + +namespace PlatformThemeEvents +{ +// To avoid the overhead of Qt's signal/slot connections, we use custom events +// to communicate with subclasses. This way, we can indicate what actually +// changed without needing to add new virtual functions to PlatformTheme which +// would break binary compatibility. +// +// To handle these events in your subclass, override QObject::event() and check +// if you receive one of these events, then do what is needed. Finally, make +// sure to call PlatformTheme::event() since that will also do some processing +// of these events. + +template +class KIRIGAMIPLATFORM_EXPORT PropertyChangedEvent : public QEvent +{ +public: + PropertyChangedEvent(PlatformTheme *theme, const T &previous, const T ¤t) + : QEvent(PropertyChangedEvent::type) + , sender(theme) + , oldValue(previous) + , newValue(current) + { + } + + PlatformTheme *sender; + T oldValue; + T newValue; + + static QEvent::Type type; +}; + +using DataChangedEvent = PropertyChangedEvent>; +using ColorSetChangedEvent = PropertyChangedEvent; +using ColorGroupChangedEvent = PropertyChangedEvent; +using ColorChangedEvent = PropertyChangedEvent; +using FontChangedEvent = PropertyChangedEvent; + +} + +} +} // namespace Kirigami + +Q_DECLARE_OPERATORS_FOR_FLAGS(Kirigami::Platform::PlatformThemeChangeTracker::PropertyChanges) + +#endif // PLATFORMTHEME_H diff --git a/src/platform/settings.cpp b/src/platform/settings.cpp new file mode 100644 index 0000000..2da0d53 --- /dev/null +++ b/src/platform/settings.cpp @@ -0,0 +1,242 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "settings.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "kirigamiplatform_version.h" +#include "smoothscrollwatcher.h" +#include "tabletmodewatcher.h" + +namespace Kirigami +{ +namespace Platform +{ + +class SettingsSingleton +{ +public: + Settings self; +}; + +Settings::Settings(QObject *parent) + : QObject(parent) + , m_hasTouchScreen(false) + , m_hasTransientTouchInput(false) +{ + m_tabletModeAvailable = TabletModeWatcher::self()->isTabletModeAvailable(); + connect(TabletModeWatcher::self(), &TabletModeWatcher::tabletModeAvailableChanged, this, [this](bool tabletModeAvailable) { + setTabletModeAvailable(tabletModeAvailable); + }); + + m_tabletMode = TabletModeWatcher::self()->isTabletMode(); + connect(TabletModeWatcher::self(), &TabletModeWatcher::tabletModeChanged, this, [this](bool tabletMode) { + setTabletMode(tabletMode); + }); + +#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(UBUNTU_TOUCH) + m_mobile = true; + m_hasTouchScreen = true; +#else + // Mostly for debug purposes and for platforms which are always mobile, + // such as Plasma Mobile + if (qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MOBILE")) { + m_mobile = QByteArrayList{"1", "true"}.contains(qgetenv("QT_QUICK_CONTROLS_MOBILE")); + } else { + m_mobile = false; + } + + const auto touchDevices = QInputDevice::devices(); + const auto touchDeviceType = QInputDevice::DeviceType::TouchScreen; + for (const auto &device : touchDevices) { + if (device->type() == touchDeviceType) { + m_hasTouchScreen = true; + break; + } + } + if (m_hasTouchScreen) { + connect(qApp, &QGuiApplication::focusWindowChanged, this, [this](QWindow *win) { + if (win) { + win->installEventFilter(this); + } + }); + } +#endif + + auto bar = QGuiApplicationPrivate::platformTheme()->createPlatformMenuBar(); + m_hasPlatformMenuBar = bar != nullptr; + if (bar != nullptr) { + bar->deleteLater(); + } + + const QString configPath = QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("kdeglobals")); + if (QFile::exists(configPath)) { + QSettings globals(configPath, QSettings::IniFormat); + globals.beginGroup(QStringLiteral("KDE")); + m_scrollLines = qMax(1, globals.value(QStringLiteral("WheelScrollLines"), 3).toInt()); + m_smoothScroll = globals.value(QStringLiteral("SmoothScroll"), true).toBool(); + } else { + m_scrollLines = 3; + m_smoothScroll = true; + } + + connect(SmoothScrollWatcher::self(), &SmoothScrollWatcher::enabledChanged, this, [this](bool enabled) { + m_smoothScroll = enabled; + Q_EMIT smoothScrollChanged(); + }); +} + +Settings::~Settings() +{ +} + +bool Settings::eventFilter(QObject *watched, QEvent *event) +{ + Q_UNUSED(watched) + switch (event->type()) { + case QEvent::TouchBegin: + setTransientTouchInput(true); + break; + case QEvent::MouseButtonPress: + case QEvent::MouseMove: { + QMouseEvent *me = static_cast(event); + if (me->source() == Qt::MouseEventNotSynthesized) { + setTransientTouchInput(false); + } + break; + } + case QEvent::Wheel: + setTransientTouchInput(false); + default: + break; + } + + return false; +} + +void Settings::setTabletModeAvailable(bool mobileAvailable) +{ + if (mobileAvailable == m_tabletModeAvailable) { + return; + } + + m_tabletModeAvailable = mobileAvailable; + Q_EMIT tabletModeAvailableChanged(); +} + +bool Settings::isTabletModeAvailable() const +{ + return m_tabletModeAvailable; +} + +void Settings::setIsMobile(bool mobile) +{ + if (mobile == m_mobile) { + return; + } + + m_mobile = mobile; + Q_EMIT isMobileChanged(); +} + +bool Settings::isMobile() const +{ + return m_mobile; +} + +void Settings::setTabletMode(bool tablet) +{ + if (tablet == m_tabletMode) { + return; + } + + m_tabletMode = tablet; + Q_EMIT tabletModeChanged(); +} + +bool Settings::tabletMode() const +{ + return m_tabletMode; +} + +void Settings::setTransientTouchInput(bool touch) +{ + if (touch == m_hasTransientTouchInput) { + return; + } + + m_hasTransientTouchInput = touch; + if (!m_tabletMode) { + Q_EMIT hasTransientTouchInputChanged(); + } +} + +bool Settings::hasTransientTouchInput() const +{ + return m_hasTransientTouchInput || m_tabletMode; +} + +QString Settings::style() const +{ + return m_style; +} + +void Settings::setStyle(const QString &style) +{ + m_style = style; +} + +int Settings::mouseWheelScrollLines() const +{ + return m_scrollLines; +} + +bool Settings::smoothScroll() const +{ + return m_smoothScroll; +} + +QStringList Settings::information() const +{ + return { +#ifndef KIRIGAMI_BUILD_TYPE_STATIC + tr("KDE Frameworks %1").arg(QStringLiteral(KIRIGAMIPLATFORM_VERSION_STRING)), +#endif + tr("The %1 windowing system").arg(QGuiApplication::platformName()), + tr("Qt %2 (built against %3)").arg(QString::fromLocal8Bit(qVersion()), QStringLiteral(QT_VERSION_STR))}; +} + +QVariant Settings::applicationWindowIcon() const +{ + const QIcon &windowIcon = qApp->windowIcon(); + if (windowIcon.isNull()) { + return QVariant(); + } + return windowIcon; +} + +bool Settings::hasPlatformMenuBar() const +{ + return m_hasPlatformMenuBar; +} + +} +} + +#include "moc_settings.cpp" diff --git a/src/platform/settings.h b/src/platform/settings.h new file mode 100644 index 0000000..f44363d --- /dev/null +++ b/src/platform/settings.h @@ -0,0 +1,157 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ +#ifndef SETTINGS_H +#define SETTINGS_H + +#include +#include +#include + +#include "kirigamiplatform_export.h" + +namespace Kirigami +{ +namespace Platform +{ +/** + * This class contains global kirigami settings about the current device setup + * It is exposed to QML as the singleton "Settings" + */ +class KIRIGAMIPLATFORM_EXPORT Settings : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + /** + * This property holds whether the system can dynamically enter and exit tablet mode + * (or the device is actually a tablet). + * This is the case for foldable convertibles and transformable laptops that support + * keyboard detachment. + */ + Q_PROPERTY(bool tabletModeAvailable READ isTabletModeAvailable NOTIFY tabletModeAvailableChanged FINAL) + + /** + * This property holds whether the application is running on a small mobile device + * such as a mobile phone. This is used when we want to do specific adaptations to + * the UI for small screen form factors, such as having bigger touch areas. + */ + Q_PROPERTY(bool isMobile READ isMobile NOTIFY isMobileChanged FINAL) + + /** + * This property holds whether the application is running on a device that is + * behaving like a tablet. + * + * @note This doesn't mean exactly a tablet form factor, but + * that the preferred input mode for the device is the touch screen + * and that pointer and keyboard are either secondary or not available. + */ + Q_PROPERTY(bool tabletMode READ tabletMode NOTIFY tabletModeChanged FINAL) + + /** + * This property holds whether the system has a platform menu bar; e.g. a user is + * on macOS or has a global menu on KDE Plasma. + * + * @warning Android has a platform menu bar; which may not be what you expected. + */ + Q_PROPERTY(bool hasPlatformMenuBar READ hasPlatformMenuBar CONSTANT FINAL) + + /** + * This property holds whether the user in this moment is interacting with the app + * with the touch screen. + */ + Q_PROPERTY(bool hasTransientTouchInput READ hasTransientTouchInput NOTIFY hasTransientTouchInputChanged FINAL) + + /** + * This property holds the name of the QtQuickControls2 style the application is using, + * for instance org.kde.desktop, Plasma, Material, Universal etc + */ + Q_PROPERTY(QString style READ style CONSTANT FINAL) + + // TODO: make this adapt without file watchers? + /** + * This property holds the number of lines of text the mouse wheel should scroll. + */ + Q_PROPERTY(int mouseWheelScrollLines READ mouseWheelScrollLines CONSTANT FINAL) + + /** + * This property holds whether to display animated transitions when scrolling with a + * mouse wheel or the keyboard. + */ + Q_PROPERTY(bool smoothScroll READ smoothScroll NOTIFY smoothScrollChanged FINAL) + + /** + * This property holds the runtime information about the libraries in use. + * + * @since 5.52 + * @since org.kde.kirigami 2.6 + */ + Q_PROPERTY(QStringList information READ information CONSTANT FINAL) + + /** + * This property holds the name of the application window icon. + * @see QGuiApplication::windowIcon() + * + * @since 5.62 + * @since org.kde.kirigami 2.10 + */ + Q_PROPERTY(QVariant applicationWindowIcon READ applicationWindowIcon CONSTANT FINAL) + +public: + Settings(QObject *parent = nullptr); + ~Settings() override; + + void setTabletModeAvailable(bool mobile); + bool isTabletModeAvailable() const; + + void setIsMobile(bool mobile); + bool isMobile() const; + + void setTabletMode(bool tablet); + bool tabletMode() const; + + void setTransientTouchInput(bool touch); + bool hasTransientTouchInput() const; + + bool hasPlatformMenuBar() const; + + QString style() const; + void setStyle(const QString &style); + + int mouseWheelScrollLines() const; + + bool smoothScroll() const; + + QStringList information() const; + + QVariant applicationWindowIcon() const; + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + +Q_SIGNALS: + void tabletModeAvailableChanged(); + void tabletModeChanged(); + void isMobileChanged(); + void hasTransientTouchInputChanged(); + void smoothScrollChanged(); + +private: + QString m_style; + int m_scrollLines = 0; + bool m_smoothScroll : 1; + bool m_tabletModeAvailable : 1; + bool m_mobile : 1; + bool m_tabletMode : 1; + bool m_hasTouchScreen : 1; + bool m_hasTransientTouchInput : 1; + bool m_hasPlatformMenuBar : 1; +}; + +} +} + +#endif diff --git a/src/platform/smoothscrollwatcher.cpp b/src/platform/smoothscrollwatcher.cpp new file mode 100644 index 0000000..af72443 --- /dev/null +++ b/src/platform/smoothscrollwatcher.cpp @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2024 Nathan Misner + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "smoothscrollwatcher.h" + +#ifdef KIRIGAMI_ENABLE_DBUS +#include +#endif + +#include "kirigamiplatform_logging.h" + +namespace Kirigami +{ +namespace Platform +{ +Q_GLOBAL_STATIC(SmoothScrollWatcher, smoothScrollWatcherSelf) + +SmoothScrollWatcher::SmoothScrollWatcher(QObject *parent) + : QObject(parent) +{ +#ifdef KIRIGAMI_ENABLE_DBUS + QDBusConnection::sessionBus().connect(QStringLiteral(""), + QStringLiteral("/SmoothScroll"), + QStringLiteral("org.kde.SmoothScroll"), + QStringLiteral("notifyChange"), + this, + SLOT(setEnabled(bool))); +#endif + m_enabled = true; +} + +SmoothScrollWatcher::~SmoothScrollWatcher() = default; + +bool SmoothScrollWatcher::enabled() const +{ + return m_enabled; +} + +SmoothScrollWatcher *SmoothScrollWatcher::self() +{ + return smoothScrollWatcherSelf(); +} + +void SmoothScrollWatcher::setEnabled(bool value) +{ + m_enabled = value; + Q_EMIT enabledChanged(value); +} + +} +} + +#include "moc_smoothscrollwatcher.cpp" diff --git a/src/platform/smoothscrollwatcher.h b/src/platform/smoothscrollwatcher.h new file mode 100644 index 0000000..882d049 --- /dev/null +++ b/src/platform/smoothscrollwatcher.h @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2024 Nathan Misner + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef KIRIGAMI_SMOOTHSCROLLWATCHER_H +#define KIRIGAMI_SMOOTHSCROLLWATCHER_H + +#include + +#include "kirigamiplatform_export.h" + +namespace Kirigami +{ +namespace Platform +{ +/** + * @class SmoothScrollWatcher smoothscrollwatcher.h + * + * This class reports on the status of the SmoothScroll DBus interface, + * which sends a message when the smooth scroll setting gets changed. + */ +class KIRIGAMIPLATFORM_EXPORT SmoothScrollWatcher : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged FINAL) + +public: + SmoothScrollWatcher(QObject *parent = nullptr); + ~SmoothScrollWatcher(); + + bool enabled() const; + + static SmoothScrollWatcher *self(); + +Q_SIGNALS: + void enabledChanged(bool value); + +private: + bool m_enabled; + +private Q_SLOTS: + void setEnabled(bool value); +}; + +} +} + +#endif // KIRIGAMI_SMOOTHSCROLLWATCHER_H diff --git a/src/platform/styleselector.cpp b/src/platform/styleselector.cpp new file mode 100644 index 0000000..cea8649 --- /dev/null +++ b/src/platform/styleselector.cpp @@ -0,0 +1,115 @@ +/* + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "styleselector.h" + +#include +#include +#include +#include + +namespace Kirigami +{ +namespace Platform +{ + +QString StyleSelector::style() +{ + if (qEnvironmentVariableIntValue("KIRIGAMI_FORCE_STYLE") == 1) { + return QQuickStyle::name(); + } else { + return styleChain().first(); + } +} + +QStringList StyleSelector::styleChain() +{ + if (qEnvironmentVariableIntValue("KIRIGAMI_FORCE_STYLE") == 1) { + return {QQuickStyle::name()}; + } + + if (!s_styleChain.isEmpty()) { + return s_styleChain; + } + + auto style = QQuickStyle::name(); + +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + // org.kde.desktop.plasma is a couple of files that fall back to desktop by purpose + if (style.isEmpty() || style == QStringLiteral("org.kde.desktop.plasma")) { + auto path = resolveFilePath(QStringLiteral("/styles/org.kde.desktop")); + if (QFile::exists(path)) { + s_styleChain.prepend(QStringLiteral("org.kde.desktop")); + } + } +#elif defined(Q_OS_ANDROID) + s_styleChain.prepend(QStringLiteral("Material")); +#else // do we have an iOS specific style? + s_styleChain.prepend(QStringLiteral("Material")); +#endif + + auto stylePath = resolveFilePath(QStringLiteral("/styles/") + style); + if (!style.isEmpty() && QFile::exists(stylePath) && !s_styleChain.contains(style)) { + s_styleChain.prepend(style); + // if we have plasma deps installed, use them for extra integration + auto plasmaPath = resolveFilePath(QStringLiteral("/styles/org.kde.desktop.plasma")); + if (style == QStringLiteral("org.kde.desktop") && QFile::exists(plasmaPath)) { + s_styleChain.prepend(QStringLiteral("org.kde.desktop.plasma")); + } + } else { +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + s_styleChain.prepend(QStringLiteral("org.kde.desktop")); +#endif + } + + return s_styleChain; +} + +QUrl StyleSelector::componentUrl(const QString &fileName) +{ + const auto chain = styleChain(); + for (const QString &style : chain) { + const QString candidate = QStringLiteral("styles/") + style + QLatin1Char('/') + fileName; + if (QFile::exists(resolveFilePath(candidate))) { + return QUrl(resolveFileUrl(candidate)); + } + } + + if (!QFile::exists(resolveFilePath(fileName))) { + qCWarning(KirigamiPlatform) << "Requested an unexisting component" << fileName; + } + return QUrl(resolveFileUrl(fileName)); +} + +void StyleSelector::setBaseUrl(const QUrl &baseUrl) +{ + s_baseUrl = baseUrl; +} + +QString StyleSelector::resolveFilePath(const QString &path) +{ +#if defined(KIRIGAMI_BUILD_TYPE_STATIC) || defined(Q_OS_ANDROID) + return QStringLiteral(":/qt/qml/org/kde/kirigami/") + path; +#else + if (s_baseUrl.isValid()) { + return s_baseUrl.toLocalFile() + QLatin1Char('/') + path; + } else { + return QDir::currentPath() + QLatin1Char('/') + path; + } +#endif +} + +QString StyleSelector::resolveFileUrl(const QString &path) +{ +#if defined(KIRIGAMI_BUILD_TYPE_STATIC) || defined(Q_OS_ANDROID) + return QStringLiteral("qrc:/qt/qml/org/kde/kirigami/") + path; +#else + return s_baseUrl.toString() + QLatin1Char('/') + path; +#endif +} + +} +} diff --git a/src/platform/styleselector.h b/src/platform/styleselector.h new file mode 100644 index 0000000..bfac31c --- /dev/null +++ b/src/platform/styleselector.h @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef STYLESELECTOR_H +#define STYLESELECTOR_H + +#include +#include + +#include "kirigamiplatform_export.h" + +class QUrl; + +namespace Kirigami +{ +namespace Platform +{ + +class KIRIGAMIPLATFORM_EXPORT StyleSelector +{ +public: + static QString style(); + static QStringList styleChain(); + + static QUrl componentUrl(const QString &fileName); + + static void setBaseUrl(const QUrl &baseUrl); + + static QString resolveFilePath(const QString &path); + static QString resolveFileUrl(const QString &path); + +private: + inline static QUrl s_baseUrl; + inline static QStringList s_styleChain; +}; + +} +} + +#endif // STYLESELECTOR_H diff --git a/src/platform/tabletmodewatcher.cpp b/src/platform/tabletmodewatcher.cpp new file mode 100644 index 0000000..f214178 --- /dev/null +++ b/src/platform/tabletmodewatcher.cpp @@ -0,0 +1,164 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * SPDX-FileCopyrightText: 2023 Harald Sitter + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "tabletmodewatcher.h" +#include + +#if defined(KIRIGAMI_ENABLE_DBUS) +#include "settings_interface.h" +#include +#endif + +using namespace Qt::Literals::StringLiterals; + +// TODO: All the dbus stuff should be conditional, optional win32 support + +namespace Kirigami +{ +namespace Platform +{ + +class TabletModeWatcherSingleton +{ +public: + TabletModeWatcher self; +}; + +Q_GLOBAL_STATIC(TabletModeWatcherSingleton, privateTabletModeWatcherSelf) + +class TabletModeWatcherPrivate +{ + static constexpr auto PORTAL_GROUP = "org.kde.TabletMode"_L1; + static constexpr auto KEY_AVAILABLE = "available"_L1; + static constexpr auto KEY_ENABLED = "enabled"_L1; + +public: + TabletModeWatcherPrivate(TabletModeWatcher *watcher) + : q(watcher) + { + // Called here to avoid collisions with application event types so we should use + // registerEventType for generating the event types. + TabletModeChangedEvent::type = QEvent::Type(QEvent::registerEventType()); +#if !defined(KIRIGAMI_ENABLE_DBUS) && (defined(Q_OS_ANDROID) || defined(Q_OS_IOS)) + isTabletModeAvailable = true; + isTabletMode = true; +#elif defined(KIRIGAMI_ENABLE_DBUS) + // Mostly for debug purposes and for platforms which are always mobile, + // such as Plasma Mobile + if (qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MOBILE") || qEnvironmentVariableIsSet("KDE_KIRIGAMI_TABLET_MODE")) { + isTabletMode = (QString::fromLatin1(qgetenv("QT_QUICK_CONTROLS_MOBILE")) == QLatin1String("1") + || QString::fromLatin1(qgetenv("QT_QUICK_CONTROLS_MOBILE")) == QLatin1String("true")) + || (QString::fromLatin1(qgetenv("KDE_KIRIGAMI_TABLET_MODE")) == QLatin1String("1") + || QString::fromLatin1(qgetenv("KDE_KIRIGAMI_TABLET_MODE")) == QLatin1String("true")); + isTabletModeAvailable = isTabletMode; + } else if (qEnvironmentVariableIsSet("QT_NO_XDG_DESKTOP_PORTAL")) { + isTabletMode = false; + } else { + qDBusRegisterMetaType(); + auto portal = new OrgFreedesktopPortalSettingsInterface(u"org.freedesktop.portal.Desktop"_s, + u"/org/freedesktop/portal/desktop"_s, + QDBusConnection::sessionBus(), + q); + + QObject::connect(portal, + &OrgFreedesktopPortalSettingsInterface::SettingChanged, + q, + [this](const QString &group, const QString &key, const QDBusVariant &value) { + if (group != PORTAL_GROUP) { + return; + } + if (key == KEY_AVAILABLE) { + Q_EMIT q->tabletModeAvailableChanged(value.variant().toBool()); + } else if (key == KEY_ENABLED) { + setIsTablet(value.variant().toBool()); + } + }); + + auto readAllProps = [portal, this] { + const auto reply = portal->ReadAll({PORTAL_GROUP}); + auto watcher = new QDBusPendingCallWatcher(reply, q); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, [this, watcher]() { + watcher->deleteLater(); + QDBusPendingReply reply = *watcher; + const auto properties = reply.value().value(PORTAL_GROUP); + Q_EMIT q->tabletModeAvailableChanged(properties[KEY_AVAILABLE].toBool()); + setIsTablet(properties[KEY_ENABLED].toBool()); + }); + }; + // If app.exec() has not been called yet, give Qt the chance to register us with the host portal + if (QThread::currentThread()->loopLevel() == 0) { + QMetaObject::invokeMethod(q, readAllProps, Qt::QueuedConnection); + } else { + readAllProps(); + } + } +// TODO: case for Windows +#endif + } + ~TabletModeWatcherPrivate() = default; + void setIsTablet(bool tablet); + + TabletModeWatcher *q; + QList watchers; + bool isTabletModeAvailable = false; + bool isTabletMode = false; +}; + +void TabletModeWatcherPrivate::setIsTablet(bool tablet) +{ + if (isTabletMode == tablet) { + return; + } + + isTabletMode = tablet; + TabletModeChangedEvent event{tablet}; + Q_EMIT q->tabletModeChanged(tablet); + for (auto *w : watchers) { + QCoreApplication::sendEvent(w, &event); + } +} + +TabletModeWatcher::TabletModeWatcher(QObject *parent) + : QObject(parent) + , d(new TabletModeWatcherPrivate(this)) +{ +} + +TabletModeWatcher::~TabletModeWatcher() +{ + delete d; +} + +TabletModeWatcher *TabletModeWatcher::self() +{ + return &privateTabletModeWatcherSelf()->self; +} + +bool TabletModeWatcher::isTabletModeAvailable() const +{ + return d->isTabletModeAvailable; +} + +bool TabletModeWatcher::isTabletMode() const +{ + return d->isTabletMode; +} + +void TabletModeWatcher::addWatcher(QObject *watcher) +{ + d->watchers.append(watcher); +} + +void TabletModeWatcher::removeWatcher(QObject *watcher) +{ + d->watchers.removeAll(watcher); +} + +} +} + +#include "moc_tabletmodewatcher.cpp" diff --git a/src/platform/tabletmodewatcher.h b/src/platform/tabletmodewatcher.h new file mode 100644 index 0000000..d405ae0 --- /dev/null +++ b/src/platform/tabletmodewatcher.h @@ -0,0 +1,100 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef KIRIGAMI_TABLETMODEWATCHER_H +#define KIRIGAMI_TABLETMODEWATCHER_H + +#include +#include + +#include "kirigamiplatform_export.h" + +namespace Kirigami +{ +namespace Platform +{ +class TabletModeWatcherPrivate; + +class KIRIGAMIPLATFORM_EXPORT TabletModeChangedEvent : public QEvent +{ +public: + TabletModeChangedEvent(bool tablet) + : QEvent(TabletModeChangedEvent::type) + , tabletMode(tablet) + { + } + + bool tabletMode = false; + + inline static QEvent::Type type = QEvent::None; +}; + +/** + * @class TabletModeWatcher tabletmodewatcher.h + * + * This class reports on the status of certain transformable + * devices which can be both tablets and laptops at the same time, + * with a detachable keyboard. + * It reports whether the device supports a tablet mode and if + * the device is currently in such mode or not, emitting a signal + * when the user switches. + */ +class KIRIGAMIPLATFORM_EXPORT TabletModeWatcher : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool tabletModeAvailable READ isTabletModeAvailable NOTIFY tabletModeAvailableChanged FINAL) + Q_PROPERTY(bool tabletMode READ isTabletMode NOTIFY tabletModeChanged FINAL) + +public: + ~TabletModeWatcher() override; + static TabletModeWatcher *self(); + + /** + * @returns true if the device supports a tablet mode and has a switch + * to report when the device has been transformed. + * For debug purposes, if either the environment variable QT_QUICK_CONTROLS_MOBILE + * or KDE_KIRIGAMI_TABLET_MODE are set to true, isTabletModeAvailable will be true + */ + bool isTabletModeAvailable() const; + + /** + * @returns true if the machine is now in tablet mode, such as the + * laptop keyboard flipped away or detached. + * Note that this doesn't mean exactly a tablet form factor, but + * that the preferred input mode for the device is the touch screen + * and that pointer and keyboard are either secondary or not available. + * + * For debug purposes, if either the environment variable QT_QUICK_CONTROLS_MOBILE + * or KDE_KIRIGAMI_TABLET_MODE are set to true, isTabletMode will be true + */ + bool isTabletMode() const; + + /** + * Register an arbitrary QObject to send events from this. + * At the moment only one event will be sent: TabletModeChangedEvent + */ + void addWatcher(QObject *watcher); + + /* + * Unsubscribe watcher from receiving events from TabletModeWatcher. + */ + void removeWatcher(QObject *watcher); + +Q_SIGNALS: + void tabletModeAvailableChanged(bool tabletModeAvailable); + void tabletModeChanged(bool tabletMode); + +private: + KIRIGAMIPLATFORM_NO_EXPORT explicit TabletModeWatcher(QObject *parent = nullptr); + TabletModeWatcherPrivate *d; + friend class TabletModeWatcherSingleton; +}; + +} +} + +#endif // KIRIGAMI_TABLETMODEWATCHER diff --git a/src/platform/units.cpp b/src/platform/units.cpp new file mode 100644 index 0000000..0591e87 --- /dev/null +++ b/src/platform/units.cpp @@ -0,0 +1,361 @@ +/* + * SPDX-FileCopyrightText: 2020 Jonah Brüchert + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "units.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "kirigamiplatform_logging.h" +#include "platformpluginfactory.h" + +namespace Kirigami +{ +namespace Platform +{ + +class UnitsPrivate +{ + Q_DISABLE_COPY(UnitsPrivate) + +public: + explicit UnitsPrivate(Units *units) + // Cache font so we don't have to go through QVariant and property every time + : fontMetrics(QFontMetricsF(QGuiApplication::font())) + , gridUnit(18) + , smallSpacing(4) + , mediumSpacing(6) + , largeSpacing(8) + , veryLongDuration(400) + , longDuration(200) + , shortDuration(100) + , veryShortDuration(50) + , humanMoment(2000) + , toolTipDelay(700) + , cornerRadius(5) + , iconSizes(new IconSizes(units)) + { + } + + // Font metrics used for Units. + // TextMetrics uses QFontMetricsF internally, so this should do the same + QFontMetricsF fontMetrics; + + // units + int gridUnit; + int smallSpacing; + int mediumSpacing; + int largeSpacing; + + // durations + int veryLongDuration; + int longDuration; + int shortDuration; + int veryShortDuration; + int humanMoment; + int toolTipDelay; + qreal cornerRadius; + + IconSizes *const iconSizes; + + // To prevent overriding custom set units if the font changes + bool customUnitsSet = false; +}; + +Units::~Units() = default; + +Units::Units(QObject *parent) + : QObject(parent) + , d(std::make_unique(this)) +{ + qGuiApp->installEventFilter(this); +} + +int Units::gridUnit() const +{ + return d->gridUnit; +} + +void Units::setGridUnit(int size) +{ + if (d->gridUnit == size) { + return; + } + + d->gridUnit = size; + d->customUnitsSet = true; + Q_EMIT gridUnitChanged(); +} + +int Units::smallSpacing() const +{ + return d->smallSpacing; +} + +void Units::setSmallSpacing(int size) +{ + if (d->smallSpacing == size) { + return; + } + + d->smallSpacing = size; + d->customUnitsSet = true; + Q_EMIT smallSpacingChanged(); +} + +int Units::mediumSpacing() const +{ + return d->mediumSpacing; +} + +void Units::setMediumSpacing(int size) +{ + if (d->mediumSpacing == size) { + return; + } + + d->mediumSpacing = size; + d->customUnitsSet = true; + Q_EMIT mediumSpacingChanged(); +} + +int Units::largeSpacing() const +{ + return d->largeSpacing; +} + +void Units::setLargeSpacing(int size) +{ + if (d->largeSpacing) { + return; + } + + d->largeSpacing = size; + d->customUnitsSet = true; + Q_EMIT largeSpacingChanged(); +} + +int Units::veryLongDuration() const +{ + return d->veryLongDuration; +} + +void Units::setVeryLongDuration(int duration) +{ + if (d->veryLongDuration == duration) { + return; + } + + d->veryLongDuration = duration; + Q_EMIT veryLongDurationChanged(); +} + +int Units::longDuration() const +{ + return d->longDuration; +} + +void Units::setLongDuration(int duration) +{ + if (d->longDuration == duration) { + return; + } + + d->longDuration = duration; + Q_EMIT longDurationChanged(); +} + +int Units::shortDuration() const +{ + return d->shortDuration; +} + +void Units::setShortDuration(int duration) +{ + if (d->shortDuration == duration) { + return; + } + + d->shortDuration = duration; + Q_EMIT shortDurationChanged(); +} + +int Units::veryShortDuration() const +{ + return d->veryShortDuration; +} + +void Units::setVeryShortDuration(int duration) +{ + if (d->veryShortDuration == duration) { + return; + } + + d->veryShortDuration = duration; + Q_EMIT veryShortDurationChanged(); +} + +int Units::humanMoment() const +{ + return d->humanMoment; +} + +void Units::setHumanMoment(int duration) +{ + if (d->humanMoment == duration) { + return; + } + + d->humanMoment = duration; + Q_EMIT humanMomentChanged(); +} + +int Units::toolTipDelay() const +{ + return d->toolTipDelay; +} + +void Units::setToolTipDelay(int delay) +{ + if (d->toolTipDelay == delay) { + return; + } + + d->toolTipDelay = delay; + Q_EMIT toolTipDelayChanged(); +} + +qreal Units::cornerRadius() const +{ + return d->cornerRadius; +} + +void Units::setcornerRadius(qreal cornerRadius) +{ + if (d->cornerRadius == cornerRadius) { + return; + } + + d->cornerRadius = cornerRadius; + Q_EMIT cornerRadiusChanged(); +} + +Units *Units::create(QQmlEngine *qmlEngine, [[maybe_unused]] QJSEngine *jsEngine) +{ +#ifndef KIRIGAMI_BUILD_TYPE_STATIC + const QString pluginName = qmlEngine->property("_kirigamiTheme").toString(); + + auto plugin = PlatformPluginFactory::findPlugin(pluginName); + if (!plugin && !pluginName.isEmpty()) { + plugin = PlatformPluginFactory::findPlugin(); + } + + if (plugin) { + return plugin->createUnits(qmlEngine); + } +#endif + // Fall back to the default units implementation + return new Units(qmlEngine); +} + +bool Units::eventFilter([[maybe_unused]] QObject *watched, QEvent *event) +{ + if (event->type() == QEvent::ApplicationFontChange) { + d->fontMetrics = QFontMetricsF(qGuiApp->font()); + + if (d->customUnitsSet) { + return false; + } + + Q_EMIT d->iconSizes->sizeForLabelsChanged(); + } + return false; +} + +IconSizes *Units::iconSizes() const +{ + return d->iconSizes; +} + +IconSizes::IconSizes(Units *units) + : QObject(units) + , m_units(units) +{ +} + +int IconSizes::roundedIconSize(int size) const +{ + if (size < 16) { + return size; + } + + if (size < 22) { + return 16; + } + + if (size < 32) { + return 22; + } + + if (size < 48) { + return 32; + } + + if (size < 64) { + return 48; + } + + return size; +} + +int IconSizes::sizeForLabels() const +{ + // gridUnit is the height of textMetrics + return roundedIconSize(m_units->d->fontMetrics.height()); +} + +int IconSizes::small() const +{ + return 16; +} + +int IconSizes::smallMedium() const +{ + return 22; +} + +int IconSizes::medium() const +{ + return 32; +} + +int IconSizes::large() const +{ + return 48; +} + +int IconSizes::huge() const +{ + return 64; +} + +int IconSizes::enormous() const +{ + return 128; +} +} +} + +#include "moc_units.cpp" diff --git a/src/platform/units.h b/src/platform/units.h new file mode 100644 index 0000000..be12d7f --- /dev/null +++ b/src/platform/units.h @@ -0,0 +1,265 @@ +/* + * SPDX-FileCopyrightText: 2021 Jonah Brüchert + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef KIRIGAMI_UNITS_H +#define KIRIGAMI_UNITS_H + +#include + +#include +#include + +#include "kirigamiplatform_export.h" + +class QQmlEngine; + +namespace Kirigami +{ +namespace Platform +{ +class Units; +class UnitsPrivate; + +/** + * @class IconSizes units.h + * + * Provides access to platform-dependent icon sizing + */ +class KIRIGAMIPLATFORM_EXPORT IconSizes : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("Grouped Property") + + Q_PROPERTY(int sizeForLabels READ sizeForLabels NOTIFY sizeForLabelsChanged FINAL) + Q_PROPERTY(int small READ small NOTIFY smallChanged FINAL) + Q_PROPERTY(int smallMedium READ smallMedium NOTIFY smallMediumChanged FINAL) + Q_PROPERTY(int medium READ medium NOTIFY mediumChanged FINAL) + Q_PROPERTY(int large READ large NOTIFY largeChanged FINAL) + Q_PROPERTY(int huge READ huge NOTIFY hugeChanged FINAL) + Q_PROPERTY(int enormous READ enormous NOTIFY enormousChanged FINAL) + +public: + IconSizes(Units *units); + + int sizeForLabels() const; + int small() const; + int smallMedium() const; + int medium() const; + int large() const; + int huge() const; + int enormous() const; + + Q_INVOKABLE int roundedIconSize(int size) const; + +private: + KIRIGAMIPLATFORM_NO_EXPORT float iconScaleFactor() const; + + Units *m_units; + +Q_SIGNALS: + void sizeForLabelsChanged(); + void smallChanged(); + void smallMediumChanged(); + void mediumChanged(); + void largeChanged(); + void hugeChanged(); + void enormousChanged(); +}; + +/** + * @class Units units.h + * + * A set of values to define semantically sizes and durations. + */ +class KIRIGAMIPLATFORM_EXPORT Units : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + friend class IconSizes; + + /** + * The fundamental unit of space that should be used for sizes, expressed in pixels. + */ + Q_PROPERTY(int gridUnit READ gridUnit NOTIFY gridUnitChanged FINAL) + + /** + * units.iconSizes provides access to platform-dependent icon sizing + * + * The icon sizes provided are normalized for different DPI, so icons + * will scale depending on the DPI. + * + * * sizeForLabels (the largest icon size that fits within fontMetrics.height) @since 5.80 @since org.kde.kirigami 2.16 + * * small + * * smallMedium + * * medium + * * large + * * huge + * * enormous + */ + Q_PROPERTY(Kirigami::Platform::IconSizes *iconSizes READ iconSizes CONSTANT FINAL) + + /** + * This property holds the amount of spacing that should be used between smaller UI elements, + * such as a small icon and a label in a button. + */ + Q_PROPERTY(int smallSpacing READ smallSpacing NOTIFY smallSpacingChanged FINAL) + + /** + * This property holds the amount of spacing that should be used between medium UI elements, + * such as buttons and text fields in a toolbar. + */ + Q_PROPERTY(int mediumSpacing READ mediumSpacing NOTIFY mediumSpacingChanged FINAL) + + /** + * This property holds the amount of spacing that should be used between bigger UI elements, + * such as a large icon and a heading in a card. + */ + Q_PROPERTY(int largeSpacing READ largeSpacing NOTIFY largeSpacingChanged FINAL) + + /** + * units.veryLongDuration should be used for specialty animations that benefit + * from being even longer than longDuration. + */ + Q_PROPERTY(int veryLongDuration READ veryLongDuration NOTIFY veryLongDurationChanged FINAL) + + /** + * units.longDuration should be used for longer, screen-covering animations, for opening and + * closing of dialogs and other "not too small" animations + */ + Q_PROPERTY(int longDuration READ longDuration NOTIFY longDurationChanged FINAL) + + /** + * units.shortDuration should be used for short animations, such as accentuating a UI event, + * hover events, etc.. + */ + Q_PROPERTY(int shortDuration READ shortDuration NOTIFY shortDurationChanged FINAL) + + /** + * units.veryShortDuration should be used for elements that should have a hint of smoothness, + * but otherwise animate near instantly. + */ + Q_PROPERTY(int veryShortDuration READ veryShortDuration NOTIFY veryShortDurationChanged FINAL) + + /** + * Time in milliseconds equivalent to the theoretical human moment, which can be used + * to determine whether how long to wait until the user should be informed of something, + * or can be used as the limit for how long something should wait before being + * automatically initiated. + * + * Some examples: + * + * - When the user types text in a search field, wait no longer than this duration after + * the user completes typing before starting the search + * - When loading data which would commonly arrive rapidly enough to not require interaction, + * wait this long before showing a spinner + * + * This might seem an arbitrary number, but given the psychological effect that three + * seconds seems to be what humans consider a moment (and in the case of waiting for + * something to happen, a moment is that time when you think "this is taking a bit long, + * isn't it?"), the idea is to postpone for just before such a conceptual moment. The reason + * for the two seconds, rather than three, is to function as a middle ground: Not long enough + * that the user would think that something has taken too long, for also not so fast as to + * happen too soon. + * + * See also + * https://www.psychologytoday.com/blog/all-about-addiction/201101/tick-tock-tick-hugs-and-life-in-3-second-intervals + * (the actual paper is hidden behind an academic paywall and consequently not readily + * available to us, so the source will have to be the blog entry above) + * + * \note This should __not__ be used as an animation duration, as it is deliberately not scaled according + * to the animation settings. This is specifically for determining when something has taken too long and + * the user should expect some kind of feedback. See veryShortDuration, shortDuration, longDuration, and + * veryLongDuration for animation duration choices. + * + * @since 5.81 + * @since org.kde.kirigami 2.16 + */ + Q_PROPERTY(int humanMoment READ humanMoment NOTIFY humanMomentChanged FINAL) + + /** + * time in ms by which the display of tooltips will be delayed. + * + * @sa ToolTip.delay property + */ + Q_PROPERTY(int toolTipDelay READ toolTipDelay NOTIFY toolTipDelayChanged FINAL) + + /** + * Corner radius value shared by buttons and other rectangle elements + * + * @since 6.2 + */ + Q_PROPERTY(qreal cornerRadius READ cornerRadius NOTIFY cornerRadiusChanged FINAL) + +public: + ~Units() override; + + int gridUnit() const; + void setGridUnit(int size); + + int smallSpacing() const; + void setSmallSpacing(int size); + + int mediumSpacing() const; + void setMediumSpacing(int size); + + int largeSpacing() const; + void setLargeSpacing(int size); + + int veryLongDuration() const; + void setVeryLongDuration(int duration); + + int longDuration() const; + void setLongDuration(int duration); + + int shortDuration() const; + void setShortDuration(int duration); + + int veryShortDuration() const; + void setVeryShortDuration(int duration); + + int humanMoment() const; + void setHumanMoment(int duration); + + int toolTipDelay() const; + void setToolTipDelay(int delay); + + qreal cornerRadius() const; + void setcornerRadius(qreal cornerRadius); + + IconSizes *iconSizes() const; + + static Units *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine); + +Q_SIGNALS: + void gridUnitChanged(); + void smallSpacingChanged(); + void mediumSpacingChanged(); + void largeSpacingChanged(); + void veryLongDurationChanged(); + void longDurationChanged(); + void shortDurationChanged(); + void veryShortDurationChanged(); + void humanMomentChanged(); + void toolTipDelayChanged(); + void wheelScrollLinesChanged(); + void cornerRadiusChanged(); + +protected: + explicit Units(QObject *parent = nullptr); + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + std::unique_ptr d; +}; + +} +} + +#endif diff --git a/src/platform/virtualkeyboardwatcher.cpp b/src/platform/virtualkeyboardwatcher.cpp new file mode 100644 index 0000000..ff36de9 --- /dev/null +++ b/src/platform/virtualkeyboardwatcher.cpp @@ -0,0 +1,189 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * SPDX-FileCopyrightText: 2023 Harald Sitter + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "virtualkeyboardwatcher.h" + +#ifdef KIRIGAMI_ENABLE_DBUS +#include "settings_interface.h" +#include +#include +#endif + +#include "kirigamiplatform_logging.h" + +using namespace Qt::Literals::StringLiterals; + +namespace Kirigami +{ +namespace Platform +{ +Q_GLOBAL_STATIC(VirtualKeyboardWatcher, virtualKeyboardWatcherSelf) + +class KIRIGAMIPLATFORM_NO_EXPORT VirtualKeyboardWatcher::Private +{ + static constexpr auto serviceName = "org.freedesktop.portal.Desktop"_L1; + static constexpr auto objectName = "/org/freedesktop/portal/desktop"_L1; + static constexpr auto interfaceName = "org.kde.kwin.VirtualKeyboard"_L1; + + static constexpr auto GROUP = "org.kde.VirtualKeyboard"_L1; + static constexpr auto KEY_AVAILABLE = "available"_L1; + static constexpr auto KEY_ENABLED = "enabled"_L1; + static constexpr auto KEY_ACTIVE = "active"_L1; + static constexpr auto KEY_VISIBLE = "visible"_L1; + static constexpr auto KEY_WILL_SHOW_ON_ACTIVE = "willShowOnActive"_L1; + +public: + Private(VirtualKeyboardWatcher *qq) + : q(qq) + { +#ifdef KIRIGAMI_ENABLE_DBUS + qDBusRegisterMetaType(); + settingsInterface = new OrgFreedesktopPortalSettingsInterface(serviceName, objectName, QDBusConnection::sessionBus(), q); + + QObject::connect(settingsInterface, + &OrgFreedesktopPortalSettingsInterface::SettingChanged, + q, + [this](const QString &group, const QString &key, const QDBusVariant &value) { + if (group != GROUP) { + return; + } + + if (key == KEY_AVAILABLE) { + available = value.variant().toBool(); + Q_EMIT q->availableChanged(); + } else if (key == KEY_ENABLED) { + enabled = value.variant().toBool(); + Q_EMIT q->enabledChanged(); + } else if (key == KEY_ACTIVE) { + active = value.variant().toBool(); + Q_EMIT q->activeChanged(); + } else if (key == KEY_VISIBLE) { + visible = value.variant().toBool(); + Q_EMIT q->visibleChanged(); + } else if (key == KEY_WILL_SHOW_ON_ACTIVE) { + willShowOnActive = value.variant().toBool(); + } + }); + + getAllProperties(); +#endif + } + + VirtualKeyboardWatcher *q; + +#ifdef KIRIGAMI_ENABLE_DBUS + void getAllProperties(); + void updateWillShowOnActive(); + + OrgFreedesktopPortalSettingsInterface *settingsInterface = nullptr; + + QDBusPendingCallWatcher *willShowOnActiveCall = nullptr; +#endif + + bool available = false; + bool enabled = false; + bool active = false; + bool visible = false; + bool willShowOnActive = false; +}; + +VirtualKeyboardWatcher::VirtualKeyboardWatcher(QObject *parent) + : QObject(parent) + , d(std::make_unique(this)) +{ +} + +VirtualKeyboardWatcher::~VirtualKeyboardWatcher() = default; + +bool VirtualKeyboardWatcher::available() const +{ + return d->available; +} + +bool VirtualKeyboardWatcher::enabled() const +{ + return d->enabled; +} + +bool VirtualKeyboardWatcher::active() const +{ + return d->active; +} + +bool VirtualKeyboardWatcher::visible() const +{ + return d->visible; +} + +bool VirtualKeyboardWatcher::willShowOnActive() const +{ +#ifdef KIRIGAMI_ENABLE_DBUS + d->updateWillShowOnActive(); +#endif + return d->willShowOnActive; +} + +VirtualKeyboardWatcher *VirtualKeyboardWatcher::self() +{ + return virtualKeyboardWatcherSelf(); +} + +#ifdef KIRIGAMI_ENABLE_DBUS + +void VirtualKeyboardWatcher::Private::updateWillShowOnActive() +{ + if (willShowOnActiveCall) { + return; + } + + willShowOnActiveCall = new QDBusPendingCallWatcher(settingsInterface->Read(GROUP, KEY_WILL_SHOW_ON_ACTIVE), q); + connect(willShowOnActiveCall, &QDBusPendingCallWatcher::finished, q, [this](auto call) { + QDBusPendingReply reply = *call; + if (reply.isError()) { + qCDebug(KirigamiPlatform) << reply.error().message(); + } else { + if (reply.value().toBool() != willShowOnActive) { + willShowOnActive = reply.value().toBool(); + Q_EMIT q->willShowOnActiveChanged(); + } + } + call->deleteLater(); + willShowOnActiveCall = nullptr; + }); +} + +void VirtualKeyboardWatcher::Private::getAllProperties() +{ + auto call = new QDBusPendingCallWatcher(settingsInterface->ReadAll({GROUP}), q); + connect(call, &QDBusPendingCallWatcher::finished, q, [this](auto call) { + QDBusPendingReply reply = *call; + if (reply.isError()) { + qCDebug(KirigamiPlatform) << reply.error().message(); + } else { + const auto groupValues = reply.value().value(GROUP); + available = groupValues.value(KEY_AVAILABLE).toBool(); + enabled = groupValues.value(KEY_ENABLED).toBool(); + active = groupValues.value(KEY_ACTIVE).toBool(); + visible = groupValues.value(KEY_VISIBLE).toBool(); + willShowOnActive = groupValues.value(KEY_WILL_SHOW_ON_ACTIVE).toBool(); + } + call->deleteLater(); + + Q_EMIT q->availableChanged(); + Q_EMIT q->enabledChanged(); + Q_EMIT q->activeChanged(); + Q_EMIT q->visibleChanged(); + }); +} + +#endif + +} +} + +#include "moc_virtualkeyboardwatcher.cpp" diff --git a/src/platform/virtualkeyboardwatcher.h b/src/platform/virtualkeyboardwatcher.h new file mode 100644 index 0000000..78045c8 --- /dev/null +++ b/src/platform/virtualkeyboardwatcher.h @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef KIRIGAMI_VIRTUALKEYBOARDWATCHER_H +#define KIRIGAMI_VIRTUALKEYBOARDWATCHER_H + +#include + +#include + +#include "kirigamiplatform_export.h" + +namespace Kirigami +{ +namespace Platform +{ +/** + * @class VirtualKeyboardWatcher virtualkeyboardwatcher.h + * + * This class reports on the status of KWin's VirtualKeyboard DBus interface. + * + * @since 5.91 + */ +class KIRIGAMIPLATFORM_EXPORT VirtualKeyboardWatcher : public QObject +{ + Q_OBJECT + +public: + VirtualKeyboardWatcher(QObject *parent = nullptr); + ~VirtualKeyboardWatcher(); + + Q_PROPERTY(bool available READ available NOTIFY availableChanged FINAL) + bool available() const; + Q_SIGNAL void availableChanged(); + + Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged FINAL) + bool enabled() const; + Q_SIGNAL void enabledChanged(); + + Q_PROPERTY(bool active READ active NOTIFY activeChanged FINAL) + bool active() const; + Q_SIGNAL void activeChanged(); + + Q_PROPERTY(bool visible READ visible NOTIFY visibleChanged FINAL) + bool visible() const; + Q_SIGNAL void visibleChanged(); + + Q_PROPERTY(bool willShowOnActive READ willShowOnActive NOTIFY willShowOnActiveChanged FINAL) + bool willShowOnActive() const; + Q_SIGNAL void willShowOnActiveChanged(); + + static VirtualKeyboardWatcher *self(); + +private: + class Private; + const std::unique_ptr d; +}; + +} +} + +#endif // KIRIGAMI_VIRTUALKEYBOARDWATCHER diff --git a/src/primitives/CMakeLists.txt b/src/primitives/CMakeLists.txt new file mode 100644 index 0000000..96e8afd --- /dev/null +++ b/src/primitives/CMakeLists.txt @@ -0,0 +1,87 @@ + +add_library(KirigamiPrimitives) +ecm_add_qml_module(KirigamiPrimitives URI "org.kde.kirigami.primitives" + VERSION 2.0 + GENERATE_PLUGIN_SOURCE + INSTALLED_PLUGIN_TARGET KF6KirigamiPrimitives + DEPENDENCIES QtQuick org.kde.kirigami.platform +) + +target_sources(KirigamiPrimitives PRIVATE + icon.cpp + icon.h + shadowedrectangle.cpp + shadowedrectangle.h + shadowedtexture.cpp + shadowedtexture.h + + scenegraph/managedtexturenode.cpp + scenegraph/managedtexturenode.h + scenegraph/paintedrectangleitem.cpp + scenegraph/paintedrectangleitem.h + scenegraph/shadowedborderrectanglematerial.cpp + scenegraph/shadowedborderrectanglematerial.h + scenegraph/shadowedbordertexturematerial.cpp + scenegraph/shadowedbordertexturematerial.h + scenegraph/shadowedrectanglematerial.cpp + scenegraph/shadowedrectanglematerial.h + scenegraph/shadowedrectanglenode.cpp + scenegraph/shadowedrectanglenode.h + scenegraph/shadowedtexturematerial.cpp + scenegraph/shadowedtexturematerial.h + scenegraph/shadowedtexturenode.cpp + scenegraph/shadowedtexturenode.h +) + +ecm_target_qml_sources(KirigamiPrimitives SOURCES + Separator.qml + ShadowedImage.qml +) + +set_target_properties(KirigamiPrimitives PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION 6 + EXPORT_NAME "KirigamiPrimitives" +) + +target_include_directories(KirigamiPrimitives PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) + +target_link_libraries(KirigamiPrimitives PRIVATE Qt6::Quick KirigamiPlatform) + +if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + set(_extra_options DEBUGINFO) +else() + set(_extra_options PRECOMPILE OPTIMIZED) +endif() + +qt6_add_shaders(KirigamiPrimitives "shaders" + BATCHABLE + PREFIX "/qt/qml/org/kde/kirigami/primitives/shaders" + FILES + shaders/shadowedrectangle.vert + shaders/shadowedrectangle.frag + shaders/shadowedrectangle_lowpower.frag + shaders/shadowedborderrectangle.frag + shaders/shadowedborderrectangle_lowpower.frag + shaders/shadowedtexture.frag + shaders/shadowedtexture_lowpower.frag + shaders/shadowedbordertexture.frag + shaders/shadowedbordertexture_lowpower.frag + OUTPUTS + shadowedrectangle.vert.qsb + shadowedrectangle.frag.qsb + shadowedrectangle_lowpower.frag.qsb + shadowedborderrectangle.frag.qsb + shadowedborderrectangle_lowpower.frag.qsb + shadowedtexture.frag.qsb + shadowedtexture_lowpower.frag.qsb + shadowedbordertexture.frag.qsb + shadowedbordertexture_lowpower.frag.qsb + ${_extra_options} + OUTPUT_TARGETS _out_targets +) + +ecm_finalize_qml_module(KirigamiPrimitives EXPORT KirigamiTargets) + +install(TARGETS KirigamiPrimitives ${_out_targets} EXPORT KirigamiTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS}) + diff --git a/src/primitives/README.md b/src/primitives/README.md new file mode 100644 index 0000000..417990f --- /dev/null +++ b/src/primitives/README.md @@ -0,0 +1,13 @@ +# Kirigami Primitives Module + +This module contains types considered primitives, things that provide some basic +capability like rendering a certain shape. They don't require styling or at most +read a color value from the platform. + +# What goes here + +The following criteria should be used to determine if a type belongs here: + +- Types used as building blocks for other types. +- Types are allowed to depend only on QtQuick. +- Types are only allowed to depend on the Platform submodule. diff --git a/src/primitives/Separator.qml b/src/primitives/Separator.qml new file mode 100644 index 0000000..909b170 --- /dev/null +++ b/src/primitives/Separator.qml @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2012 Marco Martin + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick + +import org.kde.kirigami.platform as Platform + +/** + * @brief A visual separator. + * + * Useful for splitting one set of items from another. + * + * @inherit QtQuick.Rectangle + */ +Rectangle { + id: root + implicitHeight: 1 + implicitWidth: 1 + Accessible.role: Accessible.Separator + Accessible.focusable: false + + enum Weight { + Light, + Normal + } + + /** + * @brief This property holds the visual weight of the separator. + * + * Weight options: + * * ``Kirigami.Separator.Weight.Light`` + * * ``Kirigami.Separator.Weight.Normal`` + * + * default: ``Kirigami.Separator.Weight.Normal`` + * + * @since 5.72 + * @since org.kde.kirigami 2.12 + */ + property int weight: Separator.Weight.Normal + + /* TODO: If we get a separator color role, change this to + * mix weights lower than Normal with the background color + * and mix weights higher than Normal with the text color. + */ + color: Platform.ColorUtils.linearInterpolation( + Platform.Theme.backgroundColor, + Platform.Theme.textColor, + weight === Separator.Weight.Light ? Platform.Theme.lightFrameContrast : Platform.Theme.frameContrast + ) +} diff --git a/src/primitives/ShadowedImage.qml b/src/primitives/ShadowedImage.qml new file mode 100644 index 0000000..c2cac12 --- /dev/null +++ b/src/primitives/ShadowedImage.qml @@ -0,0 +1,154 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * SPDX-FileCopyrightText: 2022 Carl Schwan + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +/** + * @brief An image with a shadow. + * + * This item will render a image, with a shadow below it. The rendering is done + * using distance fields, which provide greatly improved performance. The shadow is + * rendered outside of the item's bounds, so the item's width and height are the + * don't include the shadow. + * + * Example usage: + * @code + * import org.kde.kirigami + * + * ShadowedImage { + * source: 'qrc:/myKoolGearPicture.png' + * + * radius: 20 + * + * shadow.size: 20 + * shadow.xOffset: 5 + * shadow.yOffset: 5 + * + * border.width: 2 + * border.color: Kirigami.Theme.textColor + * + * corners.topLeftRadius: 4 + * corners.topRightRadius: 5 + * corners.bottomLeftRadius: 2 + * corners.bottomRightRadius: 10 + * } + * @endcode + * + * @since 5.69 + * @since 2.12 + * @inherit Item + */ +Item { + id: root + +//BEGIN properties + /** + * @brief This property holds the color that will be underneath the image. + * + * This will be visible if the image has transparancy. + * + * @see org::kde::kirigami::ShadowedRectangle::radius + * @property color color + */ + property alias color: shadowRectangle.color + + /** + * @brief This propery holds the corner radius of the image. + * @see org::kde::kirigami::ShadowedRectangle::radius + * @property real radius + */ + property alias radius: shadowRectangle.radius + + /** + * @brief This property holds shadow's properties group. + * @see org::kde::kirigami::ShadowedRectangle::shadow + * @property org::kde::kirigami::ShadowedRectangle::ShadowGroup shadow + */ + property alias shadow: shadowRectangle.shadow + + /** + * @brief This propery holds the border's properties of the image. + * @see org::kde::kirigami::ShadowedRectangle::border + * @property org::kde::kirigami::ShadowedRectangle::BorderGroup border + */ + property alias border: shadowRectangle.border + + /** + * @brief This propery holds the corner radius properties of the image. + * @see org::kde::kirigami::ShadowedRectangle::corners + * @property org::kde::kirigami::ShadowedRectangle::CornersGroup corners + */ + property alias corners: shadowRectangle.corners + + /** + * @brief This propery holds the source of the image. + * @brief QtQuick.Image::source + */ + property alias source: image.source + + /** + * @brief This property sets whether this image should be loaded asynchronously. + * + * Set this to false if you want the main thread to load the image, which + * blocks it until the image is loaded. Setting this to true loads the + * image in a separate thread which is useful when maintaining a responsive + * user interface is more desirable than having images immediately visible. + * + * @see QtQuick.Image::asynchronous + * @property bool asynchronous + */ + property alias asynchronous: image.asynchronous + + /** + * @brief This property defines what happens when the source image has a different + * size than the item. + * @see QtQuick.Image::fillMode + * @property int fillMode + */ + property alias fillMode: image.fillMode + + /** + * @brief This property holds whether the image uses mipmap filtering when scaled + * or transformed. + * @see QtQuick.Image::mipmap + * @property bool mipmap + */ + property alias mipmap: image.mipmap + + /** + * @brief This property holds the scaled width and height of the full-frame image. + * @see QtQuick.Image::sourceSize + */ + property alias sourceSize: image.sourceSize + + /** + * @brief This property holds the status of image loading. + * @see QtQuick.Image::status + * @since 6.5 + */ + readonly property alias status: image.status +//END properties + + Image { + id: image + anchors.fill: parent + } + + ShaderEffectSource { + id: textureSource + sourceItem: image + hideSource: !shadowRectangle.softwareRendering + } + + Kirigami.ShadowedTexture { + id: shadowRectangle + anchors.fill: parent + source: (image.status === Image.Ready && !softwareRendering) ? textureSource : null + visible: !softwareRendering + } +} diff --git a/src/primitives/icon.cpp b/src/primitives/icon.cpp new file mode 100644 index 0000000..5072f1b --- /dev/null +++ b/src/primitives/icon.cpp @@ -0,0 +1,760 @@ +/* + * SPDX-FileCopyrightText: 2011 Marco Martin + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "icon.h" +#include "scenegraph/managedtexturenode.h" + +#include "platform/platformtheme.h" +#include "platform/units.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_GLOBAL_STATIC(ImageTexturesCache, s_iconImageCache) + +Icon::Icon(QQuickItem *parent) + : QQuickItem(parent) + , m_active(false) + , m_selected(false) + , m_isMask(false) +{ + setFlag(ItemHasContents, true); + // Using 32 because Icon used to redefine implicitWidth and implicitHeight and hardcode them to 32 + setImplicitSize(32, 32); + + connect(this, &QQuickItem::smoothChanged, this, &QQuickItem::polish); + connect(this, &QQuickItem::enabledChanged, this, [this]() { + polish(); + }); +} + +Icon::~Icon() +{ +} + +void Icon::componentComplete() +{ + QQuickItem::componentComplete(); + + QQmlEngine *engine = qmlEngine(this); + Q_ASSERT(engine); + m_units = engine->singletonInstance("org.kde.kirigami.platform", "Units"); + Q_ASSERT(m_units); + m_animation = new QPropertyAnimation(this); + connect(m_animation, &QPropertyAnimation::valueChanged, this, &Icon::valueChanged); + connect(m_animation, &QPropertyAnimation::finished, this, [this]() { + m_oldIcon = QImage(); + m_textureChanged = true; + update(); + }); + m_animation->setTargetObject(this); + m_animation->setEasingCurve(QEasingCurve::InOutCubic); + m_animation->setDuration(m_units->longDuration()); + connect(m_units, &Kirigami::Platform::Units::longDurationChanged, m_animation, [this]() { + m_animation->setDuration(m_units->longDuration()); + }); + updatePaintedGeometry(); +} + +void Icon::setSource(const QVariant &icon) +{ + if (m_source == icon) { + return; + } + m_source = icon; + + if (!m_theme) { + m_theme = static_cast(qmlAttachedPropertiesObject(this, true)); + Q_ASSERT(m_theme); + + connect(m_theme, &Kirigami::Platform::PlatformTheme::colorsChanged, this, &QQuickItem::polish); + } + + if (m_networkReply) { + // if there was a network query going on, interrupt it + m_networkReply->close(); + } + m_loadedImage = QImage(); + setStatus(Loading); + + polish(); + Q_EMIT sourceChanged(); + Q_EMIT validChanged(); +} + +QVariant Icon::source() const +{ + return m_source; +} + +void Icon::setActive(const bool active) +{ + if (active == m_active) { + return; + } + m_active = active; + polish(); + Q_EMIT activeChanged(); +} + +bool Icon::active() const +{ + return m_active; +} + +bool Icon::valid() const +{ + // TODO: should this be return m_status == Ready? + // Consider an empty URL invalid, even though isNull() will say false + if (m_source.canConvert() && m_source.toUrl().isEmpty()) { + return false; + } + + return !m_source.isNull(); +} + +void Icon::setSelected(const bool selected) +{ + if (selected == m_selected) { + return; + } + m_selected = selected; + polish(); + Q_EMIT selectedChanged(); +} + +bool Icon::selected() const +{ + return m_selected; +} + +void Icon::setIsMask(bool mask) +{ + if (m_isMask == mask) { + return; + } + + m_isMask = mask; + polish(); + Q_EMIT isMaskChanged(); +} + +bool Icon::isMask() const +{ + return m_isMask; +} + +void Icon::setColor(const QColor &color) +{ + if (m_color == color) { + return; + } + + m_color = color; + polish(); + Q_EMIT colorChanged(); +} + +QColor Icon::color() const +{ + return m_color; +} + +QSGNode *Icon::createSubtree(qreal initialOpacity) +{ + auto opacityNode = new QSGOpacityNode{}; + opacityNode->setFlag(QSGNode::OwnedByParent, true); + opacityNode->setOpacity(initialOpacity); + + auto *mNode = new ManagedTextureNode; + + mNode->setTexture(s_iconImageCache->loadTexture(window(), m_icon, QQuickWindow::TextureCanUseAtlas)); + + opacityNode->appendChildNode(mNode); + + return opacityNode; +} + +void Icon::updateSubtree(QSGNode *node, qreal opacity) +{ + auto opacityNode = static_cast(node); + opacityNode->setOpacity(opacity); + + auto textureNode = static_cast(opacityNode->firstChild()); + textureNode->setFiltering(smooth() ? QSGTexture::Linear : QSGTexture::Nearest); +} + +QSGNode *Icon::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData * /*data*/) +{ + if (m_source.isNull() || qFuzzyIsNull(width()) || qFuzzyIsNull(height())) { + delete node; + return nullptr; + } + + if (!node) { + node = new QSGNode{}; + } + + if (m_animation && m_animation->state() == QAbstractAnimation::Running) { + if (node->childCount() < 2) { + node->appendChildNode(createSubtree(0.0)); + m_textureChanged = true; + } + + // Rather than doing a perfect crossfade, first fade in the new texture + // then fade out the old texture. This is done to avoid the underlying + // color bleeding through when both textures are at ~0.5 opacity, which + // causes flickering if the two textures are very similar. + updateSubtree(node->firstChild(), 2.0 - m_animValue * 2.0); + updateSubtree(node->lastChild(), m_animValue * 2.0); + } else { + if (node->childCount() == 0) { + node->appendChildNode(createSubtree(1.0)); + m_textureChanged = true; + } + + if (node->childCount() > 1) { + auto toRemove = node->firstChild(); + node->removeChildNode(toRemove); + delete toRemove; + } + + updateSubtree(node->firstChild(), 1.0); + } + + if (m_textureChanged) { + auto mNode = static_cast(node->lastChild()->firstChild()); + mNode->setTexture(s_iconImageCache->loadTexture(window(), m_icon, QQuickWindow::TextureCanUseAtlas)); + m_textureChanged = false; + m_sizeChanged = true; + } + + if (m_sizeChanged) { + const QSizeF iconPixSize(m_icon.width() / m_devicePixelRatio, m_icon.height() / m_devicePixelRatio); + const QSizeF itemPixSize = QSizeF((size() * m_devicePixelRatio).toSize()) / m_devicePixelRatio; + QRectF nodeRect(QPoint(0, 0), itemPixSize); + + if (itemPixSize.width() != 0 && itemPixSize.height() != 0) { + if (iconPixSize != itemPixSize) { + // At this point, the image will already be scaled, but we need to output it in + // the correct aspect ratio, painted centered in the viewport. So: + QRectF destination(QPointF(0, 0), QSizeF(m_icon.size()).scaled(m_paintedSize, Qt::KeepAspectRatio)); + destination.moveCenter(nodeRect.center()); + destination.moveTopLeft(QPointF(destination.topLeft().toPoint() * m_devicePixelRatio) / m_devicePixelRatio); + nodeRect = destination; + } + } + + for (int i = 0; i < node->childCount(); ++i) { + auto mNode = static_cast(node->childAtIndex(i)->firstChild()); + mNode->setRect(nodeRect); + } + + m_sizeChanged = false; + } + + return node; +} + +void Icon::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + QQuickItem::geometryChange(newGeometry, oldGeometry); + if (newGeometry.size() != oldGeometry.size()) { + m_sizeChanged = true; + updatePaintedGeometry(); + polish(); + } +} + +void Icon::handleRedirect(QNetworkReply *reply) +{ + QNetworkAccessManager *qnam = reply->manager(); + if (reply->error() != QNetworkReply::NoError) { + return; + } + const QUrl possibleRedirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); + if (!possibleRedirectUrl.isEmpty()) { + const QUrl redirectUrl = reply->url().resolved(possibleRedirectUrl); + if (redirectUrl == reply->url()) { + // no infinite redirections thank you very much + reply->deleteLater(); + return; + } + reply->deleteLater(); + QNetworkRequest request(possibleRedirectUrl); + request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); + m_networkReply = qnam->get(request); + connect(m_networkReply.data(), &QNetworkReply::finished, this, [this]() { + handleFinished(m_networkReply); + }); + } +} + +void Icon::handleFinished(QNetworkReply *reply) +{ + if (!reply) { + return; + } + + reply->deleteLater(); + if (!reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isNull()) { + handleRedirect(reply); + return; + } + + m_loadedImage = QImage(); + + const QString filename = reply->url().fileName(); + if (!m_loadedImage.load(reply, filename.mid(filename.indexOf(QLatin1Char('.'))).toLatin1().constData())) { + // broken image from data, inform the user of this with some useful broken-image thing... + m_loadedImage = iconPixmap(QIcon::fromTheme(m_fallback)); + } + + polish(); +} + +void Icon::updatePolish() +{ + QQuickItem::updatePolish(); + + if (window()) { + m_devicePixelRatio = window()->effectiveDevicePixelRatio(); + } + + if (m_source.isNull()) { + setStatus(Ready); + updatePaintedGeometry(); + update(); + return; + } + + const QSize itemSize(width(), height()); + if (itemSize.width() != 0 && itemSize.height() != 0) { + const QSize size = itemSize; + + if (m_animation) { + m_animation->stop(); + m_oldIcon = m_icon; + } + + switch (m_source.userType()) { + case QMetaType::QPixmap: + m_icon = m_source.value().toImage(); + break; + case QMetaType::QImage: + m_icon = m_source.value(); + break; + case QMetaType::QBitmap: + m_icon = m_source.value().toImage(); + break; + case QMetaType::QIcon: { + m_icon = iconPixmap(m_source.value()); + break; + } + case QMetaType::QUrl: + case QMetaType::QString: + m_icon = findIcon(size); + break; + case QMetaType::QBrush: + // todo: fill here too? + case QMetaType::QColor: + m_icon = QImage(size, QImage::Format_Alpha8); + m_icon.fill(m_source.value()); + break; + default: + break; + } + + if (m_icon.isNull()) { + m_icon = QImage(size, QImage::Format_Alpha8); + m_icon.fill(Qt::transparent); + } + + const QColor tintColor = // + !m_color.isValid() || m_color == Qt::transparent // + ? (m_selected ? m_theme->highlightedTextColor() : m_theme->textColor()) + : m_color; + + // TODO: initialize m_isMask with icon.isMask() + if (tintColor.alpha() > 0 && isMask()) { + QPainter p(&m_icon); + p.setCompositionMode(QPainter::CompositionMode_SourceIn); + p.fillRect(m_icon.rect(), tintColor); + p.end(); + } + } + + // don't animate initial setting + bool animated = m_animated && !m_oldIcon.isNull() && !m_sizeChanged && !m_blockNextAnimation; + + if (animated && m_animation) { + m_animValue = 0.0; + m_animation->setStartValue((qreal)0); + m_animation->setEndValue((qreal)1); + m_animation->start(); + } else { + if (m_animation) { + m_animation->stop(); + } + m_animValue = 1.0; + m_blockNextAnimation = false; + } + m_textureChanged = true; + updatePaintedGeometry(); + update(); +} + +QImage Icon::findIcon(const QSize &size) +{ + QImage img; + QString iconSource = m_source.toString(); + + if (iconSource.startsWith(QLatin1String("image://"))) { + QUrl iconUrl(iconSource); + QString iconProviderId = iconUrl.host(); + // QUrl path has the "/" prefix while iconId does not + QString iconId = iconUrl.path().remove(0, 1); + + QSize actualSize; + auto engine = qmlEngine(this); + if (!engine) { + return img; + } + QQuickImageProvider *imageProvider = dynamic_cast(engine->imageProvider(iconProviderId)); + if (!imageProvider) { + return img; + } + switch (imageProvider->imageType()) { + case QQmlImageProviderBase::Image: + img = imageProvider->requestImage(iconId, &actualSize, size); + if (!img.isNull()) { + setStatus(Ready); + } + break; + case QQmlImageProviderBase::Pixmap: + img = imageProvider->requestPixmap(iconId, &actualSize, size).toImage(); + if (!img.isNull()) { + setStatus(Ready); + } + break; + case QQmlImageProviderBase::ImageResponse: { + if (!m_loadedImage.isNull()) { + setStatus(Ready); + return m_loadedImage.scaled(size, Qt::KeepAspectRatio, smooth() ? Qt::SmoothTransformation : Qt::FastTransformation); + } + QQuickAsyncImageProvider *provider = dynamic_cast(imageProvider); + auto response = provider->requestImageResponse(iconId, size); + connect(response, &QQuickImageResponse::finished, this, [iconId, response, this]() { + if (response->errorString().isEmpty()) { + QQuickTextureFactory *textureFactory = response->textureFactory(); + if (textureFactory) { + m_loadedImage = textureFactory->image(); + delete textureFactory; + } + if (m_loadedImage.isNull()) { + // broken image from data, inform the user of this with some useful broken-image thing... + m_loadedImage = iconPixmap(QIcon::fromTheme(m_fallback)); + setStatus(Error); + } else { + setStatus(Ready); + } + polish(); + } + response->deleteLater(); + }); + // Temporary icon while we wait for the real image to load... + img = iconPixmap(QIcon::fromTheme(m_placeholder)); + break; + } + case QQmlImageProviderBase::Texture: { + QQuickTextureFactory *textureFactory = imageProvider->requestTexture(iconId, &actualSize, size); + if (textureFactory) { + img = textureFactory->image(); + } + if (img.isNull()) { + // broken image from data, or the texture factory wasn't healthy, inform the user of this with some useful broken-image thing... + img = iconPixmap(QIcon::fromTheme(m_fallback)); + setStatus(Error); + } else { + setStatus(Ready); + } + break; + } + case QQmlImageProviderBase::Invalid: + // will have to investigate this more + setStatus(Error); + break; + } + } else if (iconSource.startsWith(QLatin1String("http://")) || iconSource.startsWith(QLatin1String("https://"))) { + if (!m_loadedImage.isNull()) { + setStatus(Ready); + return m_loadedImage.scaled(size, Qt::KeepAspectRatio, smooth() ? Qt::SmoothTransformation : Qt::FastTransformation); + } + const auto url = m_source.toUrl(); + QQmlEngine *engine = qmlEngine(this); + QNetworkAccessManager *qnam; + if (engine && (qnam = engine->networkAccessManager()) && (!m_networkReply || m_networkReply->url() != url)) { + QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); + m_networkReply = qnam->get(request); + connect(m_networkReply.data(), &QNetworkReply::finished, this, [this]() { + handleFinished(m_networkReply); + }); + } + // Temporary icon while we wait for the real image to load... + img = iconPixmap(QIcon::fromTheme(m_placeholder)); + } else { + if (iconSource.startsWith(QLatin1String("qrc:/"))) { + iconSource = iconSource.mid(3); + } else if (iconSource.startsWith(QLatin1String("file:/"))) { + iconSource = QUrl(iconSource).path(); + } + + const QIcon icon = loadFromTheme(iconSource); + + if (!icon.isNull()) { + img = iconPixmap(icon); + setStatus(Ready); + } + } + + if (!iconSource.isEmpty() && img.isNull()) { + setStatus(Error); + img = iconPixmap(QIcon::fromTheme(m_fallback)); + } + return img; +} + +QIcon::Mode Icon::iconMode() const +{ + if (!isEnabled()) { + return QIcon::Disabled; + } else if (m_selected) { + return QIcon::Selected; + } else if (m_active) { + return QIcon::Active; + } + return QIcon::Normal; +} + +QString Icon::fallback() const +{ + return m_fallback; +} + +void Icon::setFallback(const QString &fallback) +{ + if (m_fallback != fallback) { + m_fallback = fallback; + Q_EMIT fallbackChanged(fallback); + } +} + +QString Icon::placeholder() const +{ + return m_placeholder; +} + +void Icon::setPlaceholder(const QString &placeholder) +{ + if (m_placeholder != placeholder) { + m_placeholder = placeholder; + Q_EMIT placeholderChanged(placeholder); + } +} + +void Icon::setStatus(Status status) +{ + if (status == m_status) { + return; + } + + m_status = status; + Q_EMIT statusChanged(); +} + +Icon::Status Icon::status() const +{ + return m_status; +} + +qreal Icon::paintedWidth() const +{ + return std::round(m_paintedSize.width()); +} + +qreal Icon::paintedHeight() const +{ + return std::round(m_paintedSize.height()); +} + +QSize Icon::iconSizeHint() const +{ + if (!m_roundToIconSize) { + return QSize(width(), height()); + } else if (m_units) { + return QSize(m_units->iconSizes()->roundedIconSize(std::min(width(), height())), m_units->iconSizes()->roundedIconSize(std::min(width(), height()))); + } else { + return QSize(std::min(width(), height()), std::min(width(), height())); + } +} + +QImage Icon::iconPixmap(const QIcon &icon) const +{ + const QSize actualSize = icon.actualSize(iconSizeHint()); + QIcon sourceIcon = icon; + + // if we have a non-default theme we need to load the icon with + // the right colors + const QQmlEngine *engine = qmlEngine(this); + if (engine && !engine->property("_kirigamiTheme").toString().isEmpty()) { + const QString iconName = icon.name(); + if (!iconName.isEmpty() && QIcon::hasThemeIcon(iconName)) { + sourceIcon = loadFromTheme(iconName); + } + } + + return sourceIcon.pixmap(actualSize, m_devicePixelRatio, iconMode(), QIcon::On).toImage(); +} + +QIcon Icon::loadFromTheme(const QString &iconName) const +{ + const QColor tintColor = !m_color.isValid() || m_color == Qt::transparent ? (m_selected ? m_theme->highlightedTextColor() : m_theme->textColor()) : m_color; + return m_theme->iconFromTheme(iconName, tintColor); +} + +void Icon::updatePaintedGeometry() +{ + QSizeF newSize; + if (!m_icon.width() || !m_icon.height()) { + newSize = {0, 0}; + } else { + qreal roundedWidth = m_units ? m_units->iconSizes()->roundedIconSize(std::min(width(), height())) : 32; + roundedWidth = std::round(roundedWidth * m_devicePixelRatio) / m_devicePixelRatio; + + if (QSizeF roundedSize(roundedWidth, roundedWidth); size() == roundedSize) { + m_paintedSize = roundedSize; + m_textureChanged = true; + update(); + Q_EMIT paintedAreaChanged(); + return; + } + if (m_roundToIconSize && m_units) { + if (m_icon.width() > m_icon.height()) { + newSize = QSizeF(roundedWidth, m_icon.height() * (roundedWidth / static_cast(m_icon.width()))); + } else { + newSize = QSizeF(roundedWidth, roundedWidth); + } + } else { + const QSizeF iconPixSize(m_icon.width() / m_devicePixelRatio, m_icon.height() / m_devicePixelRatio); + + const qreal w = widthValid() ? width() : iconPixSize.width(); + const qreal widthScale = w / iconPixSize.width(); + const qreal h = heightValid() ? height() : iconPixSize.height(); + const qreal heightScale = h / iconPixSize.height(); + + if (widthScale <= heightScale) { + newSize = QSizeF(w, widthScale * iconPixSize.height()); + } else if (heightScale < widthScale) { + newSize = QSizeF(heightScale * iconPixSize.width(), h); + } + } + } + if (newSize != m_paintedSize) { + m_paintedSize = newSize; + m_textureChanged = true; + update(); + Q_EMIT paintedAreaChanged(); + } +} + +bool Icon::isAnimated() const +{ + return m_animated; +} + +void Icon::setAnimated(bool animated) +{ + if (m_animated == animated) { + return; + } + + m_animated = animated; + Q_EMIT animatedChanged(); +} + +bool Icon::roundToIconSize() const +{ + return m_roundToIconSize; +} + +void Icon::setRoundToIconSize(bool roundToIconSize) +{ + if (m_roundToIconSize == roundToIconSize) { + return; + } + + const QSizeF oldPaintedSize = m_paintedSize; + + m_roundToIconSize = roundToIconSize; + Q_EMIT roundToIconSizeChanged(); + + updatePaintedGeometry(); + if (oldPaintedSize != m_paintedSize) { + Q_EMIT paintedAreaChanged(); + m_textureChanged = true; + update(); + } +} + +void Icon::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) +{ + if (change == QQuickItem::ItemDevicePixelRatioHasChanged) { + m_blockNextAnimation = true; + if (window()) { + m_devicePixelRatio = window()->effectiveDevicePixelRatio(); + } + polish(); + } else if (change == QQuickItem::ItemSceneChange) { + if (m_window) { + disconnect(m_window.data(), &QWindow::visibleChanged, this, &Icon::windowVisibleChanged); + } + m_window = value.window; + if (m_window) { + connect(m_window.data(), &QWindow::visibleChanged, this, &Icon::windowVisibleChanged); + m_devicePixelRatio = m_window->effectiveDevicePixelRatio(); + } + } else if (change == ItemVisibleHasChanged && value.boolValue) { + m_blockNextAnimation = true; + } + QQuickItem::itemChange(change, value); +} + +void Icon::valueChanged(const QVariant &value) +{ + m_animValue = value.toReal(); + update(); +} + +void Icon::windowVisibleChanged(bool visible) +{ + if (visible) { + m_blockNextAnimation = true; + } +} + +#include "moc_icon.cpp" diff --git a/src/primitives/icon.h b/src/primitives/icon.h new file mode 100644 index 0000000..83de81e --- /dev/null +++ b/src/primitives/icon.h @@ -0,0 +1,290 @@ +/* + * SPDX-FileCopyrightText: 2011 Marco Martin + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include +#include + +#include + +class QNetworkReply; +class QQuickWindow; +class QPropertyAnimation; + +namespace Kirigami +{ +namespace Platform +{ +class PlatformTheme; +class Units; +} +} + +/** + * Class for rendering an icon in UI. + */ +class Icon : public QQuickItem +{ + Q_OBJECT + QML_ELEMENT + + /** + * The source of this icon. An `Icon` can pull from: + * + * * The icon theme: + * @include icon/IconThemeSource.qml + * * The filesystem: + * @include icon/FilesystemSource.qml + * * Remote URIs: + * @include icon/InternetSource.qml + * * Custom providers: + * @include icon/CustomSource.qml + * * Your application's bundled resources: + * @include icon/ResourceSource.qml + * + * @note See https://doc.qt.io/qt-5/qtquickcontrols2-icons.html for how to + * bundle icon themes in your application to refer to them by name instead of + * by resource URL. + * + * @note Use `fallback` to provide a fallback theme name for icons. + * + * @note Cuttlefish is a KDE application that lets you view all the icons that + * you can use for your application. It offers a number of useful features such + * as previews of their appearance across different installed themes, previews + * at different sizes, and more. You might find it a useful tool when deciding + * on which icons to use in your application. + */ + Q_PROPERTY(QVariant source READ source WRITE setSource NOTIFY sourceChanged FINAL) + + /** + * The name of a fallback icon to load from the icon theme when the `source` + * cannot be found. The default fallback icon is `"unknown"`. + * + * @include icon/Fallback.qml + * + * @note This will only be loaded if source is unavailable (e.g. it doesn't exist, or network issues have prevented loading). + */ + Q_PROPERTY(QString fallback READ fallback WRITE setFallback NOTIFY fallbackChanged FINAL) + + /** + * The name of an icon from the icon theme to show while the icon set in `source` is + * being loaded. This is primarily relevant for remote sources, or those using slow- + * loading image providers. The default temporary icon is `"image-x-icon"` + * + * @note This will only be loaded if the source is a type which can be so long-loading + * that a temporary image makes sense (e.g. a remote image, or from an ImageProvider + * of the type QQmlImageProviderBase::ImageResponse) + * + * @since 5.15 + */ + Q_PROPERTY(QString placeholder READ placeholder WRITE setPlaceholder NOTIFY placeholderChanged FINAL) + + /** + * Whether this icon will use the QIcon::Active mode when drawing the icon, + * resulting in a graphical effect being applied to the icon to indicate that + * it is currently active. + * + * This is typically used to indicate when an item is being hovered or pressed. + * + * @image html icon/active.png + * + * The color differences under the default KDE color palette, Breeze. Note + * that a dull highlight background is typically displayed behind active icons and + * it is recommended to add one if you are creating a custom component. + */ + Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged FINAL) + + /** + * Whether this icon's `source` is valid and it is being used. + */ + Q_PROPERTY(bool valid READ valid NOTIFY validChanged FINAL) + + /** + * Whether this icon will use the QIcon::Selected mode when drawing the icon, + * resulting in a graphical effect being applied to the icon to indicate that + * it is currently selected. + * + * This is typically used to indicate when a list item is currently selected. + * + * @image html icon/selected.png + * + * The color differences under the default KDE color palette, Breeze. Note + * that a blue background is typically displayed behind selected elements. + */ + Q_PROPERTY(bool selected READ selected WRITE setSelected NOTIFY selectedChanged FINAL) + + /** + * Whether this icon will be treated as a mask. When an icon is being used + * as a mask, all non-transparent colors are replaced with the color provided in the Icon's + * @link Icon::color color @endlink property. + * + * @see color + */ + Q_PROPERTY(bool isMask READ isMask WRITE setIsMask NOTIFY isMaskChanged FINAL) + + /** + * The color to use when drawing this icon when `isMask` is enabled. + * If this property is not set or is `Qt::transparent`, the icon will use + * the text or the selected text color, depending on if `selected` is set to + * true. + */ + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged FINAL) + + /** + * Whether the icon is correctly loaded, is asynchronously loading or there was an error. + * Note that image loading will not be initiated until the item is shown, so if the Icon is not visible, + * it can only have Null or Loading states. + * @since 5.15 + */ + Q_PROPERTY(Icon::Status status READ status NOTIFY statusChanged FINAL) + + /** + * The width of the painted area measured in pixels. This will be smaller than or + * equal to the width of the area taken up by the Item itself. This can be 0. + * + * @since 5.15 + */ + Q_PROPERTY(qreal paintedWidth READ paintedWidth NOTIFY paintedAreaChanged FINAL) + + /** + * The height of the painted area measured in pixels. This will be smaller than or + * equal to the height of the area taken up by the Item itself. This can be 0. + * + * @since 5.15 + */ + Q_PROPERTY(qreal paintedHeight READ paintedHeight NOTIFY paintedAreaChanged FINAL) + + /** + * If set, icon will blend when the source is changed + */ + Q_PROPERTY(bool animated READ isAnimated WRITE setAnimated NOTIFY animatedChanged FINAL) + + /** + * If set, icon will round the painted size to defined icon sizes. Default is true. + */ + Q_PROPERTY(bool roundToIconSize READ roundToIconSize WRITE setRoundToIconSize NOTIFY roundToIconSizeChanged FINAL) + +public: + enum Status { + Null = 0, /// No icon has been set + Ready, /// The icon loaded correctly + Loading, // The icon is being loaded, but not ready yet + Error, /// There was an error while loading the icon, for instance a non existent themed name, or an invalid url + }; + Q_ENUM(Status) + + Icon(QQuickItem *parent = nullptr); + ~Icon() override; + + void componentComplete() override; + + void setSource(const QVariant &source); + QVariant source() const; + + void setActive(bool active = true); + bool active() const; + + bool valid() const; + + void setSelected(bool selected = true); + bool selected() const; + + void setIsMask(bool mask); + bool isMask() const; + + void setColor(const QColor &color); + QColor color() const; + + QString fallback() const; + void setFallback(const QString &fallback); + + QString placeholder() const; + void setPlaceholder(const QString &placeholder); + + Status status() const; + + qreal paintedWidth() const; + qreal paintedHeight() const; + + bool isAnimated() const; + void setAnimated(bool animated); + + bool roundToIconSize() const; + void setRoundToIconSize(bool roundToIconSize); + + QSGNode *updatePaintNode(QSGNode *node, UpdatePaintNodeData *data) override; + +Q_SIGNALS: + void sourceChanged(); + void activeChanged(); + void validChanged(); + void selectedChanged(); + void isMaskChanged(); + void colorChanged(); + void fallbackChanged(const QString &fallback); + void placeholderChanged(const QString &placeholder); + void statusChanged(); + void paintedAreaChanged(); + void animatedChanged(); + void roundToIconSizeChanged(); + +protected: + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; + QImage findIcon(const QSize &size); + void handleFinished(QNetworkReply *reply); + void handleRedirect(QNetworkReply *reply); + QIcon::Mode iconMode() const; + bool guessMonochrome(const QImage &img); + void setStatus(Status status); + void updatePolish() override; + void updatePaintedGeometry(); + void updateIsMaskHeuristic(const QString &iconSource); + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) override; + +private: + void valueChanged(const QVariant &value); + void windowVisibleChanged(bool visible); + QSGNode *createSubtree(qreal initialOpacity); + void updateSubtree(QSGNode *node, qreal opacity); + QSize iconSizeHint() const; + inline QImage iconPixmap(const QIcon &icon) const; + QIcon loadFromTheme(const QString &iconName) const; + + Kirigami::Platform::PlatformTheme *m_theme = nullptr; + Kirigami::Platform::Units *m_units = nullptr; + QPointer m_networkReply; + QHash m_monochromeHeuristics; + QVariant m_source; + qreal m_devicePixelRatio = 1.0; + Status m_status = Null; + bool m_textureChanged = false; + bool m_sizeChanged = false; + bool m_active; + bool m_selected; + bool m_isMask; + bool m_isMaskHeuristic = false; + QImage m_loadedImage; + QColor m_color = Qt::transparent; + QString m_fallback = QStringLiteral("unknown"); + QString m_placeholder = QStringLiteral("image-png"); + QSizeF m_paintedSize; + + QImage m_oldIcon; + QImage m_icon; + + // animation on image change + QPropertyAnimation *m_animation = nullptr; + qreal m_animValue = 1.0; + bool m_animated = false; + bool m_roundToIconSize = true; + bool m_blockNextAnimation = false; + QPointer m_window; +}; diff --git a/src/primitives/scenegraph/managedtexturenode.cpp b/src/primitives/scenegraph/managedtexturenode.cpp new file mode 100644 index 0000000..5c021b1 --- /dev/null +++ b/src/primitives/scenegraph/managedtexturenode.cpp @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2011 Marco Martin + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "managedtexturenode.h" + +ManagedTextureNode::ManagedTextureNode() +{ +} + +void ManagedTextureNode::setTexture(std::shared_ptr texture) +{ + m_texture = texture; + QSGSimpleTextureNode::setTexture(texture.get()); +} + +ImageTexturesCache::ImageTexturesCache() + : d(new ImageTexturesCachePrivate) +{ +} + +ImageTexturesCache::~ImageTexturesCache() +{ +} + +std::shared_ptr ImageTexturesCache::loadTexture(QQuickWindow *window, const QImage &image, QQuickWindow::CreateTextureOptions options) +{ + qint64 id = image.cacheKey(); + std::shared_ptr texture = d->cache.value(id).value(window).lock(); + + if (!texture) { + auto cleanAndDelete = [this, window, id](QSGTexture *texture) { + QHash> &textures = (d->cache)[id]; + textures.remove(window); + if (textures.isEmpty()) { + d->cache.remove(id); + } + delete texture; + }; + texture = std::shared_ptr(window->createTextureFromImage(image, options), cleanAndDelete); + (d->cache)[id][window] = texture; + } + + // if we have a cache in an atlas but our request cannot use an atlassed texture + // create a new texture and use that + // don't use removedFromAtlas() as that requires keeping a reference to the non atlased version + if (!(options & QQuickWindow::TextureCanUseAtlas) && texture->isAtlasTexture()) { + texture = std::shared_ptr(window->createTextureFromImage(image, options)); + } + + return texture; +} + +std::shared_ptr ImageTexturesCache::loadTexture(QQuickWindow *window, const QImage &image) +{ + return loadTexture(window, image, {}); +} diff --git a/src/primitives/scenegraph/managedtexturenode.h b/src/primitives/scenegraph/managedtexturenode.h new file mode 100644 index 0000000..b65a498 --- /dev/null +++ b/src/primitives/scenegraph/managedtexturenode.h @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2011 Marco Martin + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once +#include +#include +#include +#include +#include + +class ManagedTextureNode : public QSGSimpleTextureNode +{ + Q_DISABLE_COPY(ManagedTextureNode) +public: + ManagedTextureNode(); + + void setTexture(std::shared_ptr texture); + +private: + std::shared_ptr m_texture; +}; + +typedef QHash>> TexturesCache; + +struct ImageTexturesCachePrivate { + TexturesCache cache; +}; + +class ImageTexturesCache +{ +public: + ImageTexturesCache(); + ~ImageTexturesCache(); + + /** + * @returns the texture for a given @p window and @p image. + * + * If an @p image id is the same as one already provided before, we won't create + * a new texture and return a shared pointer to the existing texture. + */ + std::shared_ptr loadTexture(QQuickWindow *window, const QImage &image, QQuickWindow::CreateTextureOptions options); + + std::shared_ptr loadTexture(QQuickWindow *window, const QImage &image); + +private: + std::unique_ptr d; +}; diff --git a/src/primitives/scenegraph/paintedrectangleitem.cpp b/src/primitives/scenegraph/paintedrectangleitem.cpp new file mode 100644 index 0000000..2aa6e5e --- /dev/null +++ b/src/primitives/scenegraph/paintedrectangleitem.cpp @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "paintedrectangleitem.h" + +#include +#include + +PaintedRectangleItem::PaintedRectangleItem(QQuickItem *parent) + : QQuickPaintedItem(parent) +{ +} + +void PaintedRectangleItem::setColor(const QColor &color) +{ + m_color = color; + update(); +} + +void PaintedRectangleItem::setRadius(qreal radius) +{ + m_radius = radius; + update(); +} + +void PaintedRectangleItem::setBorderColor(const QColor &color) +{ + m_borderColor = color; + update(); +} + +void PaintedRectangleItem::setBorderWidth(qreal width) +{ + m_borderWidth = width; + update(); +} + +void PaintedRectangleItem::paint(QPainter *painter) +{ + painter->setRenderHint(QPainter::Antialiasing, true); + painter->setPen(Qt::transparent); + + auto radius = std::min(m_radius, std::min(width(), height()) / 2); + auto borderWidth = std::floor(m_borderWidth); + + if (borderWidth > 0.0) { + painter->setBrush(m_borderColor); + painter->drawRoundedRect(0, 0, width(), height(), radius, radius); + } + + painter->setBrush(m_color); + painter->drawRoundedRect(borderWidth, borderWidth, width() - borderWidth * 2, height() - borderWidth * 2, radius, radius); +} + +#include "moc_paintedrectangleitem.cpp" diff --git a/src/primitives/scenegraph/paintedrectangleitem.h b/src/primitives/scenegraph/paintedrectangleitem.h new file mode 100644 index 0000000..8036682 --- /dev/null +++ b/src/primitives/scenegraph/paintedrectangleitem.h @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef PAINTEDRECTANGLEITEM_H +#define PAINTEDRECTANGLEITEM_H + +#include + +/** + * A rectangle with a border and rounded corners, rendered through QPainter. + * + * This is a helper used by ShadowedRectangle as fallback for when software + * rendering is used, which means our shaders cannot be used. + * + * Since we cannot actually use QSGPaintedNode, we need to do some trickery + * using QQuickPaintedItem as a child of ShadowedRectangle. + * + * \warning This item is **not** intended as a general purpose item. + */ +class PaintedRectangleItem : public QQuickPaintedItem +{ + Q_OBJECT +public: + explicit PaintedRectangleItem(QQuickItem *parent = nullptr); + + void setColor(const QColor &color); + void setRadius(qreal radius); + void setBorderColor(const QColor &color); + void setBorderWidth(qreal width); + + void paint(QPainter *painter) override; + +private: + QColor m_color; + qreal m_radius = 0.0; + QColor m_borderColor; + qreal m_borderWidth = 0.0; +}; + +#endif // PAINTEDRECTANGLEITEM_H diff --git a/src/primitives/scenegraph/shadowedborderrectanglematerial.cpp b/src/primitives/scenegraph/shadowedborderrectanglematerial.cpp new file mode 100644 index 0000000..be41113 --- /dev/null +++ b/src/primitives/scenegraph/shadowedborderrectanglematerial.cpp @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "shadowedborderrectanglematerial.h" + +#include + +QSGMaterialType ShadowedBorderRectangleMaterial::staticType; + +ShadowedBorderRectangleMaterial::ShadowedBorderRectangleMaterial() +{ + setFlag(QSGMaterial::Blending, true); +} + +QSGMaterialShader *ShadowedBorderRectangleMaterial::createShader(QSGRendererInterface::RenderMode) const +{ + return new ShadowedBorderRectangleShader{shaderType}; +} + +QSGMaterialType *ShadowedBorderRectangleMaterial::type() const +{ + return &staticType; +} + +int ShadowedBorderRectangleMaterial::compare(const QSGMaterial *other) const +{ + auto material = static_cast(other); + + auto result = ShadowedRectangleMaterial::compare(other); + /* clang-format off */ + if (result == 0 + && material->borderColor == borderColor + && qFuzzyCompare(material->borderWidth, borderWidth)) { /* clang-format on */ + return 0; + } + + return QSGMaterial::compare(other); +} + +ShadowedBorderRectangleShader::ShadowedBorderRectangleShader(ShadowedRectangleMaterial::ShaderType shaderType) + : ShadowedRectangleShader(shaderType) +{ + setShader(shaderType, QStringLiteral("shadowedborderrectangle")); +} + +bool ShadowedBorderRectangleShader::updateUniformData(QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) +{ + bool changed = ShadowedRectangleShader::updateUniformData(state, newMaterial, oldMaterial); + QByteArray *buf = state.uniformData(); + Q_ASSERT(buf->size() >= 160); + + if (!oldMaterial || newMaterial->compare(oldMaterial) != 0) { + const auto material = static_cast(newMaterial); + memcpy(buf->data() + 136, &material->borderWidth, 8); + float c[4]; + material->borderColor.getRgbF(&c[0], &c[1], &c[2], &c[3]); + memcpy(buf->data() + 144, c, 16); + changed = true; + } + + return changed; +} diff --git a/src/primitives/scenegraph/shadowedborderrectanglematerial.h b/src/primitives/scenegraph/shadowedborderrectanglematerial.h new file mode 100644 index 0000000..d7620cc --- /dev/null +++ b/src/primitives/scenegraph/shadowedborderrectanglematerial.h @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include "shadowedrectanglematerial.h" + +/** + * A material rendering a rectangle with a shadow and a border. + * + * This material uses a distance field shader to render a rectangle with a + * shadow below it, optionally with rounded corners and a border. + */ +class ShadowedBorderRectangleMaterial : public ShadowedRectangleMaterial +{ +public: + ShadowedBorderRectangleMaterial(); + + QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override; + QSGMaterialType *type() const override; + int compare(const QSGMaterial *other) const override; + + float borderWidth = 0.0; + QColor borderColor = Qt::black; + + static QSGMaterialType staticType; +}; + +class ShadowedBorderRectangleShader : public ShadowedRectangleShader +{ +public: + ShadowedBorderRectangleShader(ShadowedRectangleMaterial::ShaderType shaderType); + + bool updateUniformData(QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; +}; diff --git a/src/primitives/scenegraph/shadowedbordertexturematerial.cpp b/src/primitives/scenegraph/shadowedbordertexturematerial.cpp new file mode 100644 index 0000000..786631b --- /dev/null +++ b/src/primitives/scenegraph/shadowedbordertexturematerial.cpp @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "shadowedbordertexturematerial.h" + +#include + +QSGMaterialType ShadowedBorderTextureMaterial::staticType; + +ShadowedBorderTextureMaterial::ShadowedBorderTextureMaterial() + : ShadowedBorderRectangleMaterial() +{ + setFlag(QSGMaterial::Blending, true); +} + +QSGMaterialShader *ShadowedBorderTextureMaterial::createShader(QSGRendererInterface::RenderMode) const +{ + return new ShadowedBorderTextureShader{shaderType}; +} + +QSGMaterialType *ShadowedBorderTextureMaterial::type() const +{ + return &staticType; +} + +int ShadowedBorderTextureMaterial::compare(const QSGMaterial *other) const +{ + auto material = static_cast(other); + + auto result = ShadowedBorderRectangleMaterial::compare(other); + if (result == 0) { + if (material->textureSource == textureSource) { + return 0; + } else { + return (material->textureSource < textureSource) ? 1 : -1; + } + } + + return QSGMaterial::compare(other); +} + +ShadowedBorderTextureShader::ShadowedBorderTextureShader(ShadowedRectangleMaterial::ShaderType shaderType) + : ShadowedBorderRectangleShader(shaderType) +{ + setShader(shaderType, QStringLiteral("shadowedbordertexture")); +} + +void ShadowedBorderTextureShader::updateSampledImage(QSGMaterialShader::RenderState &state, + int binding, + QSGTexture **texture, + QSGMaterial *newMaterial, + QSGMaterial *oldMaterial) +{ + Q_UNUSED(state); + Q_UNUSED(oldMaterial); + if (binding == 1) { + *texture = static_cast(newMaterial)->textureSource; + } +} diff --git a/src/primitives/scenegraph/shadowedbordertexturematerial.h b/src/primitives/scenegraph/shadowedbordertexturematerial.h new file mode 100644 index 0000000..e644174 --- /dev/null +++ b/src/primitives/scenegraph/shadowedbordertexturematerial.h @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include + +#include "shadowedborderrectanglematerial.h" + +class ShadowedBorderTextureMaterial : public ShadowedBorderRectangleMaterial +{ +public: + ShadowedBorderTextureMaterial(); + + QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override; + QSGMaterialType *type() const override; + int compare(const QSGMaterial *other) const override; + + QSGTexture *textureSource = nullptr; + + static QSGMaterialType staticType; +}; + +class ShadowedBorderTextureShader : public ShadowedBorderRectangleShader +{ +public: + ShadowedBorderTextureShader(ShadowedRectangleMaterial::ShaderType shaderType); + + void + updateSampledImage(QSGMaterialShader::RenderState &state, int binding, QSGTexture **texture, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; +}; diff --git a/src/primitives/scenegraph/shadowedrectanglematerial.cpp b/src/primitives/scenegraph/shadowedrectanglematerial.cpp new file mode 100644 index 0000000..d5beaa6 --- /dev/null +++ b/src/primitives/scenegraph/shadowedrectanglematerial.cpp @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "shadowedrectanglematerial.h" + +#include + +QSGMaterialType ShadowedRectangleMaterial::staticType; + +ShadowedRectangleMaterial::ShadowedRectangleMaterial() +{ + setFlag(QSGMaterial::Blending, true); +} + +QSGMaterialShader *ShadowedRectangleMaterial::createShader(QSGRendererInterface::RenderMode) const +{ + return new ShadowedRectangleShader{shaderType}; +} + +QSGMaterialType *ShadowedRectangleMaterial::type() const +{ + return &staticType; +} + +int ShadowedRectangleMaterial::compare(const QSGMaterial *other) const +{ + auto material = static_cast(other); + /* clang-format off */ + if (material->color == color + && material->shadowColor == shadowColor + && material->offset == offset + && material->aspect == aspect + && qFuzzyCompare(material->size, size) + && qFuzzyCompare(material->radius, radius)) { /* clang-format on */ + return 0; + } + + return QSGMaterial::compare(other); +} + +ShadowedRectangleShader::ShadowedRectangleShader(ShadowedRectangleMaterial::ShaderType shaderType) +{ + setShader(shaderType, QStringLiteral("shadowedrectangle")); +} + +bool ShadowedRectangleShader::updateUniformData(RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) +{ + bool changed = false; + QByteArray *buf = state.uniformData(); + Q_ASSERT(buf->size() >= 160); + + if (state.isMatrixDirty()) { + const QMatrix4x4 m = state.combinedMatrix(); + memcpy(buf->data(), m.constData(), 64); + changed = true; + } + + if (state.isOpacityDirty()) { + const float opacity = state.opacity(); + memcpy(buf->data() + 72, &opacity, 4); + changed = true; + } + + if (!oldMaterial || newMaterial->compare(oldMaterial) != 0) { + const auto material = static_cast(newMaterial); + memcpy(buf->data() + 64, &material->aspect, 8); + memcpy(buf->data() + 76, &material->size, 4); + memcpy(buf->data() + 80, &material->radius, 16); + float c[4]; + material->color.getRgbF(&c[0], &c[1], &c[2], &c[3]); + memcpy(buf->data() + 96, c, 16); + material->shadowColor.getRgbF(&c[0], &c[1], &c[2], &c[3]); + memcpy(buf->data() + 112, c, 16); + memcpy(buf->data() + 128, &material->offset, 8); + changed = true; + } + + return changed; +} + +void ShadowedRectangleShader::setShader(ShadowedRectangleMaterial::ShaderType shaderType, const QString &shader) +{ + const auto shaderRoot = QStringLiteral(":/qt/qml/org/kde/kirigami/primitives/shaders/"); + + setShaderFileName(QSGMaterialShader::VertexStage, shaderRoot + QStringLiteral("shadowedrectangle.vert.qsb")); + + auto shaderFile = shader; + if (shaderType == ShadowedRectangleMaterial::ShaderType::LowPower) { + shaderFile += QStringLiteral("_lowpower"); + } + setShaderFileName(QSGMaterialShader::FragmentStage, shaderRoot + shaderFile + QStringLiteral(".frag.qsb")); +} diff --git a/src/primitives/scenegraph/shadowedrectanglematerial.h b/src/primitives/scenegraph/shadowedrectanglematerial.h new file mode 100644 index 0000000..bafcae8 --- /dev/null +++ b/src/primitives/scenegraph/shadowedrectanglematerial.h @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include + +/** + * A material rendering a rectangle with a shadow. + * + * This material uses a distance field shader to render a rectangle with a + * shadow below it, optionally with rounded corners. + */ +class ShadowedRectangleMaterial : public QSGMaterial +{ +public: + enum class ShaderType { + Standard, + LowPower, + }; + + ShadowedRectangleMaterial(); + + QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override; + QSGMaterialType *type() const override; + int compare(const QSGMaterial *other) const override; + + QVector2D aspect = QVector2D{1.0, 1.0}; + float size = 0.0; + QVector4D radius = QVector4D{0.0, 0.0, 0.0, 0.0}; + QColor color = Qt::white; + QColor shadowColor = Qt::black; + QVector2D offset; + ShaderType shaderType = ShaderType::Standard; + + static QSGMaterialType staticType; +}; + +class ShadowedRectangleShader : public QSGMaterialShader +{ +public: + ShadowedRectangleShader(ShadowedRectangleMaterial::ShaderType shaderType); + + bool updateUniformData(QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; + +protected: + void setShader(ShadowedRectangleMaterial::ShaderType shaderType, const QString &shader); +}; diff --git a/src/primitives/scenegraph/shadowedrectanglenode.cpp b/src/primitives/scenegraph/shadowedrectanglenode.cpp new file mode 100644 index 0000000..ee1e58e --- /dev/null +++ b/src/primitives/scenegraph/shadowedrectanglenode.cpp @@ -0,0 +1,207 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "shadowedrectanglenode.h" +#include "shadowedborderrectanglematerial.h" + +QColor premultiply(const QColor &color) +{ + return QColor::fromRgbF(color.redF() * color.alphaF(), // + color.greenF() * color.alphaF(), + color.blueF() * color.alphaF(), + color.alphaF()); +} + +ShadowedRectangleNode::ShadowedRectangleNode() +{ + m_geometry = new QSGGeometry{QSGGeometry::defaultAttributes_TexturedPoint2D(), 4}; + setGeometry(m_geometry); + + setFlags(QSGNode::OwnsGeometry | QSGNode::OwnsMaterial); +} + +void ShadowedRectangleNode::setBorderEnabled(bool enabled) +{ + // We can achieve more performant shaders by splitting the two into separate + // shaders. This requires separating the materials as well. So when + // borderWidth is increased to something where the border should be visible, + // switch to the with-border material. Otherwise use the no-border version. + + if (enabled) { + if (!m_material || m_material->type() == borderlessMaterialType()) { + auto newMaterial = createBorderMaterial(); + newMaterial->shaderType = m_shaderType; + setMaterial(newMaterial); + m_material = newMaterial; + m_rect = QRectF{}; + markDirty(QSGNode::DirtyMaterial); + } + } else { + if (!m_material || m_material->type() == borderMaterialType()) { + auto newMaterial = createBorderlessMaterial(); + newMaterial->shaderType = m_shaderType; + setMaterial(newMaterial); + m_material = newMaterial; + m_rect = QRectF{}; + markDirty(QSGNode::DirtyMaterial); + } + } +} + +void ShadowedRectangleNode::setRect(const QRectF &rect) +{ + if (rect == m_rect) { + return; + } + + m_rect = rect; + + QVector2D newAspect{1.0, 1.0}; + if (m_rect.width() >= m_rect.height()) { + newAspect.setX(m_rect.width() / m_rect.height()); + } else { + newAspect.setY(m_rect.height() / m_rect.width()); + } + + if (m_material->aspect != newAspect) { + m_material->aspect = newAspect; + markDirty(QSGNode::DirtyMaterial); + m_aspect = newAspect; + } +} + +void ShadowedRectangleNode::setSize(qreal size) +{ + auto minDimension = std::min(m_rect.width(), m_rect.height()); + float uniformSize = (size / minDimension) * 2.0; + + if (!qFuzzyCompare(m_material->size, uniformSize)) { + m_material->size = uniformSize; + markDirty(QSGNode::DirtyMaterial); + m_size = size; + } +} + +void ShadowedRectangleNode::setRadius(const QVector4D &radius) +{ + float minDimension = std::min(m_rect.width(), m_rect.height()); + auto uniformRadius = QVector4D{std::min(radius.x() * 2.0f / minDimension, 1.0f), + std::min(radius.y() * 2.0f / minDimension, 1.0f), + std::min(radius.z() * 2.0f / minDimension, 1.0f), + std::min(radius.w() * 2.0f / minDimension, 1.0f)}; + + if (m_material->radius != uniformRadius) { + m_material->radius = uniformRadius; + markDirty(QSGNode::DirtyMaterial); + m_radius = radius; + } +} + +void ShadowedRectangleNode::setColor(const QColor &color) +{ + auto premultiplied = premultiply(color); + if (m_material->color != premultiplied) { + m_material->color = premultiplied; + markDirty(QSGNode::DirtyMaterial); + } +} + +void ShadowedRectangleNode::setShadowColor(const QColor &color) +{ + auto premultiplied = premultiply(color); + if (m_material->shadowColor != premultiplied) { + m_material->shadowColor = premultiplied; + markDirty(QSGNode::DirtyMaterial); + } +} + +void ShadowedRectangleNode::setOffset(const QVector2D &offset) +{ + auto minDimension = std::min(m_rect.width(), m_rect.height()); + auto uniformOffset = offset / minDimension; + + if (m_material->offset != uniformOffset) { + m_material->offset = uniformOffset; + markDirty(QSGNode::DirtyMaterial); + m_offset = offset; + } +} + +void ShadowedRectangleNode::setBorderWidth(qreal width) +{ + if (m_material->type() != borderMaterialType()) { + return; + } + + auto minDimension = std::min(m_rect.width(), m_rect.height()); + float uniformBorderWidth = width / minDimension; + + auto borderMaterial = static_cast(m_material); + if (!qFuzzyCompare(borderMaterial->borderWidth, uniformBorderWidth)) { + borderMaterial->borderWidth = uniformBorderWidth; + markDirty(QSGNode::DirtyMaterial); + m_borderWidth = width; + } +} + +void ShadowedRectangleNode::setBorderColor(const QColor &color) +{ + if (m_material->type() != borderMaterialType()) { + return; + } + + auto borderMaterial = static_cast(m_material); + auto premultiplied = premultiply(color); + if (borderMaterial->borderColor != premultiplied) { + borderMaterial->borderColor = premultiplied; + markDirty(QSGNode::DirtyMaterial); + } +} + +void ShadowedRectangleNode::setShaderType(ShadowedRectangleMaterial::ShaderType type) +{ + m_shaderType = type; +} + +void ShadowedRectangleNode::updateGeometry() +{ + auto rect = m_rect; + if (m_shaderType == ShadowedRectangleMaterial::ShaderType::Standard) { + rect = rect.adjusted(-m_size * m_aspect.x(), // + -m_size * m_aspect.y(), + m_size * m_aspect.x(), + m_size * m_aspect.y()); + + auto offsetLength = m_offset.length(); + rect = rect.adjusted(-offsetLength * m_aspect.x(), // + -offsetLength * m_aspect.y(), + offsetLength * m_aspect.x(), + offsetLength * m_aspect.y()); + } + + QSGGeometry::updateTexturedRectGeometry(m_geometry, rect, QRectF{0.0, 0.0, 1.0, 1.0}); + markDirty(QSGNode::DirtyGeometry); +} + +ShadowedRectangleMaterial *ShadowedRectangleNode::createBorderlessMaterial() +{ + return new ShadowedRectangleMaterial{}; +} + +ShadowedBorderRectangleMaterial *ShadowedRectangleNode::createBorderMaterial() +{ + return new ShadowedBorderRectangleMaterial{}; +} + +QSGMaterialType *ShadowedRectangleNode::borderlessMaterialType() +{ + return &ShadowedRectangleMaterial::staticType; +} + +QSGMaterialType *ShadowedRectangleNode::borderMaterialType() +{ + return &ShadowedBorderRectangleMaterial::staticType; +} diff --git a/src/primitives/scenegraph/shadowedrectanglenode.h b/src/primitives/scenegraph/shadowedrectanglenode.h new file mode 100644 index 0000000..755f56f --- /dev/null +++ b/src/primitives/scenegraph/shadowedrectanglenode.h @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include +#include + +#include "shadowedrectanglematerial.h" + +struct QSGMaterialType; +class ShadowedBorderRectangleMaterial; + +/** + * Scene graph node for a shadowed rectangle. + * + * This node will set up the geometry and materials for a shadowed rectangle, + * optionally with rounded corners. + * + * \note You must call updateGeometry() after setting properties of this node, + * otherwise the node's state will not correctly reflect all the properties. + * + * \sa ShadowedRectangle + */ +class ShadowedRectangleNode : public QSGGeometryNode +{ +public: + ShadowedRectangleNode(); + + /** + * Set whether to draw a border. + * + * Note that this will switch between a material with or without border. + * This means this needs to be called before any other setters. + */ + void setBorderEnabled(bool enabled); + + void setRect(const QRectF &rect); + void setSize(qreal size); + void setRadius(const QVector4D &radius); + void setColor(const QColor &color); + void setShadowColor(const QColor &color); + void setOffset(const QVector2D &offset); + void setBorderWidth(qreal width); + void setBorderColor(const QColor &color); + void setShaderType(ShadowedRectangleMaterial::ShaderType type); + + /** + * Update the geometry for this node. + * + * This is done as an explicit step to avoid the geometry being recreated + * multiple times while updating properties. + */ + void updateGeometry(); + +protected: + virtual ShadowedRectangleMaterial *createBorderlessMaterial(); + virtual ShadowedBorderRectangleMaterial *createBorderMaterial(); + virtual QSGMaterialType *borderMaterialType(); + virtual QSGMaterialType *borderlessMaterialType(); + + QSGGeometry *m_geometry; + ShadowedRectangleMaterial *m_material = nullptr; + ShadowedRectangleMaterial::ShaderType m_shaderType = ShadowedRectangleMaterial::ShaderType::Standard; + +private: + QRectF m_rect; + qreal m_size = 0.0; + QVector4D m_radius = QVector4D{0.0, 0.0, 0.0, 0.0}; + QVector2D m_offset = QVector2D{0.0, 0.0}; + QVector2D m_aspect = QVector2D{1.0, 1.0}; + qreal m_borderWidth = 0.0; + QColor m_borderColor; +}; diff --git a/src/primitives/scenegraph/shadowedtexturematerial.cpp b/src/primitives/scenegraph/shadowedtexturematerial.cpp new file mode 100644 index 0000000..8cd82d2 --- /dev/null +++ b/src/primitives/scenegraph/shadowedtexturematerial.cpp @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "shadowedtexturematerial.h" + +#include + +QSGMaterialType ShadowedTextureMaterial::staticType; + +ShadowedTextureMaterial::ShadowedTextureMaterial() + : ShadowedRectangleMaterial() +{ + setFlag(QSGMaterial::Blending, true); +} + +QSGMaterialShader *ShadowedTextureMaterial::createShader(QSGRendererInterface::RenderMode) const +{ + return new ShadowedTextureShader{shaderType}; +} + +QSGMaterialType *ShadowedTextureMaterial::type() const +{ + return &staticType; +} + +int ShadowedTextureMaterial::compare(const QSGMaterial *other) const +{ + auto material = static_cast(other); + + auto result = ShadowedRectangleMaterial::compare(other); + if (result == 0) { + if (material->textureSource == textureSource) { + return 0; + } else { + return (material->textureSource < textureSource) ? 1 : -1; + } + } + + return QSGMaterial::compare(other); +} + +ShadowedTextureShader::ShadowedTextureShader(ShadowedRectangleMaterial::ShaderType shaderType) + : ShadowedRectangleShader(shaderType) +{ + setShader(shaderType, QStringLiteral("shadowedtexture")); +} + +void ShadowedTextureShader::updateSampledImage(QSGMaterialShader::RenderState &state, + int binding, + QSGTexture **texture, + QSGMaterial *newMaterial, + QSGMaterial *oldMaterial) +{ + Q_UNUSED(state); + Q_UNUSED(oldMaterial); + if (binding == 1) { + *texture = static_cast(newMaterial)->textureSource; + } +} diff --git a/src/primitives/scenegraph/shadowedtexturematerial.h b/src/primitives/scenegraph/shadowedtexturematerial.h new file mode 100644 index 0000000..00799b2 --- /dev/null +++ b/src/primitives/scenegraph/shadowedtexturematerial.h @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include + +#include "shadowedrectanglematerial.h" + +/** + * A material rendering a rectangle with a shadow. + * + * This material uses a distance field shader to render a rectangle with a + * shadow below it, optionally with rounded corners. + */ +class ShadowedTextureMaterial : public ShadowedRectangleMaterial +{ +public: + ShadowedTextureMaterial(); + + QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override; + QSGMaterialType *type() const override; + int compare(const QSGMaterial *other) const override; + + QSGTexture *textureSource = nullptr; + + static QSGMaterialType staticType; +}; + +class ShadowedTextureShader : public ShadowedRectangleShader +{ +public: + ShadowedTextureShader(ShadowedRectangleMaterial::ShaderType shaderType); + + void + updateSampledImage(QSGMaterialShader::RenderState &state, int binding, QSGTexture **texture, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; +}; diff --git a/src/primitives/scenegraph/shadowedtexturenode.cpp b/src/primitives/scenegraph/shadowedtexturenode.cpp new file mode 100644 index 0000000..0b22a4d --- /dev/null +++ b/src/primitives/scenegraph/shadowedtexturenode.cpp @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "shadowedtexturenode.h" + +#include "shadowedbordertexturematerial.h" + +template +inline void preprocessTexture(QSGMaterial *material, QSGTextureProvider *provider) +{ + auto m = static_cast(material); + // Since we handle texture coordinates differently in the shader, we + // need to remove the texture from the atlas for now. + if (provider->texture()->isAtlasTexture()) { + // Blegh, I have no idea why "removedFromAtlas" doesn't just return + // the texture when it's not an atlas. + m->textureSource = provider->texture()->removedFromAtlas(); + } else { + m->textureSource = provider->texture(); + } + if (QSGDynamicTexture *dynamic_texture = qobject_cast(m->textureSource)) { + dynamic_texture->updateTexture(); + } +} + +ShadowedTextureNode::ShadowedTextureNode() + : ShadowedRectangleNode() +{ + setFlag(QSGNode::UsePreprocess); +} + +ShadowedTextureNode::~ShadowedTextureNode() +{ + QObject::disconnect(m_textureChangeConnectionHandle); +} + +void ShadowedTextureNode::setTextureSource(QSGTextureProvider *source) +{ + if (m_textureSource == source) { + return; + } + + if (m_textureSource) { + m_textureSource->disconnect(); + } + + m_textureSource = source; + m_textureChangeConnectionHandle = QObject::connect(m_textureSource.data(), &QSGTextureProvider::textureChanged, [this] { + markDirty(QSGNode::DirtyMaterial); + }); + markDirty(QSGNode::DirtyMaterial); +} + +void ShadowedTextureNode::preprocess() +{ + if (m_textureSource && m_material && m_textureSource->texture()) { + if (m_material->type() == borderlessMaterialType()) { + preprocessTexture(m_material, m_textureSource); + } else { + preprocessTexture(m_material, m_textureSource); + } + } +} + +ShadowedRectangleMaterial *ShadowedTextureNode::createBorderlessMaterial() +{ + return new ShadowedTextureMaterial{}; +} + +ShadowedBorderRectangleMaterial *ShadowedTextureNode::createBorderMaterial() +{ + return new ShadowedBorderTextureMaterial{}; +} + +QSGMaterialType *ShadowedTextureNode::borderlessMaterialType() +{ + return &ShadowedTextureMaterial::staticType; +} + +QSGMaterialType *ShadowedTextureNode::borderMaterialType() +{ + return &ShadowedBorderTextureMaterial::staticType; +} diff --git a/src/primitives/scenegraph/shadowedtexturenode.h b/src/primitives/scenegraph/shadowedtexturenode.h new file mode 100644 index 0000000..d0b0efc --- /dev/null +++ b/src/primitives/scenegraph/shadowedtexturenode.h @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include + +#include "shadowedrectanglenode.h" +#include "shadowedtexturematerial.h" + +/** + * Scene graph node for a shadowed texture source. + * + * This node will set up the geometry and materials for a shadowed rectangle, + * optionally with rounded corners, using a supplied texture source as the color + * for the rectangle. + * + * \note You must call updateGeometry() after setting properties of this node, + * otherwise the node's state will not correctly reflect all the properties. + * + * \sa ShadowedTexture + */ +class ShadowedTextureNode : public ShadowedRectangleNode +{ +public: + ShadowedTextureNode(); + ~ShadowedTextureNode(); + + void setTextureSource(QSGTextureProvider *source); + void preprocess() override; + +private: + ShadowedRectangleMaterial *createBorderlessMaterial() override; + ShadowedBorderRectangleMaterial *createBorderMaterial() override; + QSGMaterialType *borderlessMaterialType() override; + QSGMaterialType *borderMaterialType() override; + + QPointer m_textureSource; + QMetaObject::Connection m_textureChangeConnectionHandle; +}; diff --git a/src/primitives/shaders/sdf.glsl b/src/primitives/shaders/sdf.glsl new file mode 100644 index 0000000..69402c5 --- /dev/null +++ b/src/primitives/shaders/sdf.glsl @@ -0,0 +1,240 @@ +// SPDX-FileCopyrightText: 2020 Arjen Hiemstra +// SPDX-FileCopyrightText: 2017 Inigo Quilez +// +// SPDX-License-Identifier: MIT +// +// This file is based on +// https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm + +//if not GLES +// include "desktop_header.glsl" +//else +// include "es_header.glsl" + +// A maximum point count to be used for sdf_polygon input arrays. +// Unfortunately even function inputs require a fixed size at declaration time +// for arrays, unless we were to use OpenGL 4.5. +// Since the polygon is most likely to be defined in a uniform, this should be +// at least less than MAX_FRAGMENT_UNIFORM_COMPONENTS / 2 (since we need vec2). +#define SDF_POLYGON_MAX_POINT_COUNT 400 + +/********************************* + Shapes +*********************************/ + +// Distance field for a circle. +// +// \param point A point on the distance field. +// \param radius The radius of the circle. +// +// \return The signed distance from point to the circle. If negative, point is +// inside the circle. +lowp float sdf_circle(in lowp vec2 point, in lowp float radius) +{ + return length(point) - radius; +} + +// Distance field for a triangle. +// +// \param point A point on the distance field. +// \param p0 The first vertex of the triangle. +// \param p0 The second vertex of the triangle. +// \param p0 The third vertex of the triangle. +// +// \note The ordering of the three vertices does not matter. +// +// \return The signed distance from point to triangle. If negative, point is +// inside the triangle. +lowp float sdf_triangle(in lowp vec2 point, in lowp vec2 p0, in lowp vec2 p1, in lowp vec2 p2) +{ + lowp vec2 e0 = p1 - p0; + lowp vec2 e1 = p2 - p1; + lowp vec2 e2 = p0 - p2; + + lowp vec2 v0 = point - p0; + lowp vec2 v1 = point - p1; + lowp vec2 v2 = point - p2; + + lowp vec2 pq0 = v0 - e0 * clamp( dot(v0, e0) / dot(e0, e0), 0.0, 1.0 ); + lowp vec2 pq1 = v1 - e1 * clamp( dot(v1, e1) / dot(e1, e1), 0.0, 1.0 ); + lowp vec2 pq2 = v2 - e2 * clamp( dot(v2, e2) / dot(e2, e2), 0.0, 1.0 ); + + lowp float s = sign( e0.x*e2.y - e0.y*e2.x ); + lowp vec2 d = min(min(vec2(dot(pq0,pq0), s*(v0.x*e0.y-v0.y*e0.x)), + vec2(dot(pq1,pq1), s*(v1.x*e1.y-v1.y*e1.x))), + vec2(dot(pq2,pq2), s*(v2.x*e2.y-v2.y*e2.x))); + + return -sqrt(d.x)*sign(d.y); +} + +// Distance field for a rectangle. +// +// \param point A point on the distance field. +// \param rect A vec2 with the size of the rectangle. +// +// \return The signed distance from point to rectangle. If negative, point is +// inside the rectangle. +lowp float sdf_rectangle(in lowp vec2 point, in lowp vec2 rect) +{ + lowp vec2 d = abs(point) - rect; + return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); +} + +// Distance field for a rectangle with rounded corners. +// +// \param point The point to calculate the distance of. +// \param rect The rectangle to calculate the distance of. +// \param radius A vec4 with the radius of each corner. Order is top right, bottom right, top left, bottom left. +// +// \return The signed distance from point to rectangle. If negative, point is +// inside the rectangle. +lowp float sdf_rounded_rectangle(in lowp vec2 point, in lowp vec2 rect, in lowp vec4 radius) +{ + radius.xy = (point.x > 0.0) ? radius.xy : radius.zw; + radius.x = (point.y > 0.0) ? radius.x : radius.y; + lowp vec2 d = abs(point) - rect + radius.x; + return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - radius.x; +} + +/********************* + Operators +*********************/ + +// Convert a distance field to an annular (hollow) distance field. +// +// \param sdf The result of an sdf shape to convert. +// \param thickness The thickness of the resulting shape. +// +// \return The value of sdf modified to an annular shape. +lowp float sdf_annular(in lowp float sdf, in lowp float thickness) +{ + return abs(sdf) - thickness; +} + +// Union two sdf shapes together. +// +// \param sdf1 The first sdf shape. +// \param sdf2 The second sdf shape. +// +// \return The union of sdf1 and sdf2, that is, the distance to both sdf1 and +// sdf2. +lowp float sdf_union(in lowp float sdf1, in lowp float sdf2) +{ + return min(sdf1, sdf2); +} + +// Subtract two sdf shapes. +// +// \param sdf1 The first sdf shape. +// \param sdf2 The second sdf shape. +// +// \return sdf1 with sdf2 subtracted from it. +lowp float sdf_subtract(in lowp float sdf1, in lowp float sdf2) +{ + return max(sdf1, -sdf2); +} + +// Intersect two sdf shapes. +// +// \param sdf1 The first sdf shape. +// \param sdf2 The second sdf shape. +// +// \return The intersection between sdf1 and sdf2, that is, the area where both +// sdf1 and sdf2 provide the same distance value. +lowp float sdf_intersect(in lowp float sdf1, in lowp float sdf2) +{ + return max(sdf1, sdf2); +} + +// Smoothly intersect two sdf shapes. +// +// \param sdf1 The first sdf shape. +// \param sdf2 The second sdf shape. +// \param smoothing The amount of smoothing to apply. +// +// \return A smoothed version of the intersect operation. +lowp float sdf_intersect_smooth(in lowp float sdf1, in lowp float sdf2, in lowp float smoothing) +{ + lowp float h = clamp(0.5 - 0.5 * (sdf1 - sdf2) / smoothing, 0.0, 1.0); + return mix(sdf1, sdf2, h) + smoothing * h * (1.0 - h); +} + +// Round an sdf shape. +// +// \param sdf The sdf shape to round. +// \param amount The amount of rounding to apply. +// +// \return The rounded shape of sdf. +// Note that rounding happens by basically selecting an isoline of sdf, +// therefore, the resulting shape may be larger than the input shape. +lowp float sdf_round(in lowp float sdf, in lowp float amount) +{ + return sdf - amount; +} + +// Convert an sdf shape to an outline of its shape. +// +// \param sdf The sdf shape to turn into an outline. +// +// \return The outline of sdf. +lowp float sdf_outline(in lowp float sdf) +{ + return abs(sdf); +} + +/******************** + Convenience +********************/ + +// A constant to represent a "null" value of an sdf. +// +// Since 0 is a point exactly on the outline of an sdf shape, and negative +// values are inside the shape, this uses a very large positive constant to +// indicate a value that is really far away from the actual sdf shape. +const lowp float sdf_null = 99999.0; + +// A constant for a default level of smoothing when rendering an sdf. +// +// This +const lowp float sdf_default_smoothing = 0.625; + +// Render an sdf shape alpha-blended onto an existing color. +// +// This is an overload of sdf_render(float, vec4, vec4) that allows specifying a +// blending amount and a smoothing amount. +// +// \param alpha The alpha to use for blending. +// \param smoothing The amount of smoothing to apply to the sdf. +// +lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor, in lowp float alpha, in lowp float smoothing) +{ + lowp float g = fwidth(sdf); + return mix(sourceColor, sdfColor, alpha * (1.0 - smoothstep(-smoothing * g, smoothing * g, sdf))); +} + +// Render an sdf shape. +// +// This will render the sdf shape on top of whatever source color is input, +// making sure to apply smoothing if desired. +// +// \param sdf The sdf shape to render. +// \param sourceColor The source color to render on top of. +// \param sdfColor The color to use for rendering the sdf shape. +// +// \return sourceColor with the sdf shape rendered on top. +lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor) +{ + return sdf_render(sdf, sourceColor, sdfColor, 1.0, sdf_default_smoothing); +} + +// Render an sdf shape. +// +// This is an overload of sdf_render(float, vec4, vec4) that allows specifying a +// smoothing amount. +// +// \param smoothing The amount of smoothing to apply to the sdf. +// +lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor, in lowp float smoothing) +{ + return sdf_render(sdf, sourceColor, sdfColor, 1.0, smoothing); +} diff --git a/src/primitives/shaders/sdf_lowpower.glsl b/src/primitives/shaders/sdf_lowpower.glsl new file mode 100644 index 0000000..8cdc364 --- /dev/null +++ b/src/primitives/shaders/sdf_lowpower.glsl @@ -0,0 +1,240 @@ +// SPDX-FileCopyrightText: 2020 Arjen Hiemstra +// SPDX-FileCopyrightText: 2017 Inigo Quilez +// +// SPDX-License-Identifier: MIT +// +// This file is based on +// https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm + +//if not GLES +// include "desktop_header.glsl" +//else +// include "es_header.glsl" + +// A maximum point count to be used for sdf_polygon input arrays. +// Unfortunately even function inputs require a fixed size at declaration time +// for arrays, unless we were to use OpenGL 4.5. +// Since the polygon is most likely to be defined in a uniform, this should be +// at least less than MAX_FRAGMENT_UNIFORM_COMPONENTS / 2 (since we need vec2). +#define SDF_POLYGON_MAX_POINT_COUNT 400 + +/********************************* + Shapes +*********************************/ + +// Distance field for a circle. +// +// \param point A point on the distance field. +// \param radius The radius of the circle. +// +// \return The signed distance from point to the circle. If negative, point is +// inside the circle. +lowp float sdf_circle(in lowp vec2 point, in lowp float radius) +{ + return length(point) - radius; +} + +// Distance field for a triangle. +// +// \param point A point on the distance field. +// \param p0 The first vertex of the triangle. +// \param p0 The second vertex of the triangle. +// \param p0 The third vertex of the triangle. +// +// \note The ordering of the three vertices does not matter. +// +// \return The signed distance from point to triangle. If negative, point is +// inside the triangle. +lowp float sdf_triangle(in lowp vec2 point, in lowp vec2 p0, in lowp vec2 p1, in lowp vec2 p2) +{ + lowp vec2 e0 = p1 - p0; + lowp vec2 e1 = p2 - p1; + lowp vec2 e2 = p0 - p2; + + lowp vec2 v0 = point - p0; + lowp vec2 v1 = point - p1; + lowp vec2 v2 = point - p2; + + lowp vec2 pq0 = v0 - e0 * clamp( dot(v0, e0) / dot(e0, e0), 0.0, 1.0 ); + lowp vec2 pq1 = v1 - e1 * clamp( dot(v1, e1) / dot(e1, e1), 0.0, 1.0 ); + lowp vec2 pq2 = v2 - e2 * clamp( dot(v2, e2) / dot(e2, e2), 0.0, 1.0 ); + + lowp float s = sign( e0.x*e2.y - e0.y*e2.x ); + lowp vec2 d = min(min(vec2(dot(pq0,pq0), s*(v0.x*e0.y-v0.y*e0.x)), + vec2(dot(pq1,pq1), s*(v1.x*e1.y-v1.y*e1.x))), + vec2(dot(pq2,pq2), s*(v2.x*e2.y-v2.y*e2.x))); + + return -sqrt(d.x)*sign(d.y); +} + +// Distance field for a rectangle. +// +// \param point A point on the distance field. +// \param rect A vec2 with the size of the rectangle. +// +// \return The signed distance from point to rectangle. If negative, point is +// inside the rectangle. +lowp float sdf_rectangle(in lowp vec2 point, in lowp vec2 rect) +{ + lowp vec2 d = abs(point) - rect; + return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); +} + +// Distance field for a rectangle with rounded corners. +// +// \param point The point to calculate the distance of. +// \param rect The rectangle to calculate the distance of. +// \param radius A vec4 with the radius of each corner. Order is top right, bottom right, top left, bottom left. +// +// \return The signed distance from point to rectangle. If negative, point is +// inside the rectangle. +lowp float sdf_rounded_rectangle(in lowp vec2 point, in lowp vec2 rect, in lowp vec4 radius) +{ + radius.xy = (point.x > 0.0) ? radius.xy : radius.zw; + radius.x = (point.y > 0.0) ? radius.x : radius.y; + lowp vec2 d = abs(point) - rect + radius.x; + return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - radius.x; +} + +/********************* + Operators +*********************/ + +// Convert a distance field to an annular (hollow) distance field. +// +// \param sdf The result of an sdf shape to convert. +// \param thickness The thickness of the resulting shape. +// +// \return The value of sdf modified to an annular shape. +lowp float sdf_annular(in lowp float sdf, in lowp float thickness) +{ + return abs(sdf) - thickness; +} + +// Union two sdf shapes together. +// +// \param sdf1 The first sdf shape. +// \param sdf2 The second sdf shape. +// +// \return The union of sdf1 and sdf2, that is, the distance to both sdf1 and +// sdf2. +lowp float sdf_union(in lowp float sdf1, in lowp float sdf2) +{ + return min(sdf1, sdf2); +} + +// Subtract two sdf shapes. +// +// \param sdf1 The first sdf shape. +// \param sdf2 The second sdf shape. +// +// \return sdf1 with sdf2 subtracted from it. +lowp float sdf_subtract(in lowp float sdf1, in lowp float sdf2) +{ + return max(sdf1, -sdf2); +} + +// Intersect two sdf shapes. +// +// \param sdf1 The first sdf shape. +// \param sdf2 The second sdf shape. +// +// \return The intersection between sdf1 and sdf2, that is, the area where both +// sdf1 and sdf2 provide the same distance value. +lowp float sdf_intersect(in lowp float sdf1, in lowp float sdf2) +{ + return max(sdf1, sdf2); +} + +// Smoothly intersect two sdf shapes. +// +// \param sdf1 The first sdf shape. +// \param sdf2 The second sdf shape. +// \param smoothing The amount of smoothing to apply. +// +// \return A smoothed version of the intersect operation. +lowp float sdf_intersect_smooth(in lowp float sdf1, in lowp float sdf2, in lowp float smoothing) +{ + lowp float h = clamp(0.5 - 0.5 * (sdf1 - sdf2) / smoothing, 0.0, 1.0); + return mix(sdf1, sdf2, h) + smoothing * h * (1.0 - h); +} + +// Round an sdf shape. +// +// \param sdf The sdf shape to round. +// \param amount The amount of rounding to apply. +// +// \return The rounded shape of sdf. +// Note that rounding happens by basically selecting an isoline of sdf, +// therefore, the resulting shape may be larger than the input shape. +lowp float sdf_round(in lowp float sdf, in lowp float amount) +{ + return sdf - amount; +} + +// Convert an sdf shape to an outline of its shape. +// +// \param sdf The sdf shape to turn into an outline. +// +// \return The outline of sdf. +lowp float sdf_outline(in lowp float sdf) +{ + return abs(sdf); +} + +/******************** + Convenience +********************/ + +// A constant to represent a "null" value of an sdf. +// +// Since 0 is a point exactly on the outline of an sdf shape, and negative +// values are inside the shape, this uses a very large positive constant to +// indicate a value that is really far away from the actual sdf shape. +const lowp float sdf_null = 99999.0; + +// A constant for a default level of smoothing when rendering an sdf. +// +// This +const lowp float sdf_default_smoothing = 0.625; + +// Render an sdf shape alpha-blended onto an existing color. +// +// This is an overload of sdf_render(float, vec4, vec4) that allows specifying a +// blending amount and a smoothing amount. +// +// \param alpha The alpha to use for blending. +// \param smoothing The amount of smoothing to apply to the sdf. +// +lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor, in lowp float alpha, in lowp float smoothing) +{ + lowp float g = smoothing * fwidth(sdf); + return mix(sourceColor, sdfColor, alpha * (1.0 - clamp(sdf / g, 0.0, 1.0))); +} + +// Render an sdf shape. +// +// This will render the sdf shape on top of whatever source color is input, +// making sure to apply smoothing if desired. +// +// \param sdf The sdf shape to render. +// \param sourceColor The source color to render on top of. +// \param sdfColor The color to use for rendering the sdf shape. +// +// \return sourceColor with the sdf shape rendered on top. +lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor) +{ + return sdf_render(sdf, sourceColor, sdfColor, 1.0, sdf_default_smoothing); +} + +// Render an sdf shape. +// +// This is an overload of sdf_render(float, vec4, vec4) that allows specifying a +// smoothing amount. +// +// \param smoothing The amount of smoothing to apply to the sdf. +// +lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor, in lowp float smoothing) +{ + return sdf_render(sdf, sourceColor, sdfColor, 1.0, smoothing); +} diff --git a/src/primitives/shaders/shadowedborderrectangle.frag b/src/primitives/shaders/shadowedborderrectangle.frag new file mode 100644 index 0000000..5bb51be --- /dev/null +++ b/src/primitives/shaders/shadowedborderrectangle.frag @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#version 440 + +#extension GL_GOOGLE_include_directive: enable +#include "sdf.glsl" +// See sdf.glsl for the SDF related functions. + +// This shader renders a rectangle with rounded corners and a shadow below it. +// In addition it renders a border around it. + +#include "uniforms.glsl" + +layout(location = 0) in lowp vec2 uv; +layout(location = 0) out lowp vec4 out_color; + +const lowp float minimum_shadow_radius = 0.05; + +void main() +{ + // Scaling factor that is the inverse of the amount of scaling applied to the geometry. + lowp float inverse_scale = 1.0 / (1.0 + ubuf.size + length(ubuf.offset) * 2.0); + + // Correction factor to round the corners of a larger shadow. + // We want to account for size in regards to shadow radius, so that a larger shadow is + // more rounded, but only if we are not already rounding the corners due to corner radius. + lowp vec4 size_factor = 0.5 * (minimum_shadow_radius / max(ubuf.radius, minimum_shadow_radius)); + lowp vec4 shadow_radius = ubuf.radius + ubuf.size * size_factor; + + lowp vec4 col = vec4(0.0); + + // Calculate the shadow's distance field. + lowp float shadow = sdf_rounded_rectangle(uv - ubuf.offset * 2.0 * inverse_scale, ubuf.aspect * inverse_scale, shadow_radius * inverse_scale); + // Render it, interpolating the color over the distance. + col = mix(col, ubuf.shadowColor * sign(ubuf.size), 1.0 - smoothstep(-ubuf.size * 0.5, ubuf.size * 0.5, shadow)); + + // Scale corrected corner radius + lowp vec4 corner_radius = ubuf.radius * inverse_scale; + + // Calculate the outer rectangle distance field and render it. + lowp float outer_rect = sdf_rounded_rectangle(uv, ubuf.aspect * inverse_scale, corner_radius); + + col = sdf_render(outer_rect, col, ubuf.borderColor); + + // The inner rectangle distance field is the outer reduced by twice the border size. + lowp float inner_rect = outer_rect + (ubuf.borderWidth * inverse_scale) * 2.0; + + // Finally, render the inner rectangle. + col = sdf_render(inner_rect, col, ubuf.color); + + out_color = col * ubuf.opacity; +} diff --git a/src/primitives/shaders/shadowedborderrectangle_lowpower.frag b/src/primitives/shaders/shadowedborderrectangle_lowpower.frag new file mode 100644 index 0000000..abf1c15 --- /dev/null +++ b/src/primitives/shaders/shadowedborderrectangle_lowpower.frag @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#version 440 + +#extension GL_GOOGLE_include_directive: enable +#include "sdf_lowpower.glsl" +// See sdf.glsl for the SDF related functions. + +// This is a version of shadowedborderrectangle.frag for extremely low powered +// hardware (PinePhone). It does not draw a shadow and also eliminates alpha +// blending. + +#include "uniforms.glsl" + +layout(location = 0) in lowp vec2 uv; +layout(location = 0) out lowp vec4 out_color; + +void main() +{ + lowp vec4 col = vec4(0.0); + + // Calculate the outer rectangle distance field and render it. + lowp float outer_rect = sdf_rounded_rectangle(uv, ubuf.aspect, ubuf.radius); + + col = sdf_render(outer_rect, col, ubuf.borderColor); + + // The inner distance field is the outer reduced by border width. + lowp float inner_rect = outer_rect + ubuf.borderWidth * 2.0; + + // Render it. + col = sdf_render(inner_rect, col, ubuf.color); + + out_color = col * ubuf.opacity; +} diff --git a/src/primitives/shaders/shadowedbordertexture.frag b/src/primitives/shaders/shadowedbordertexture.frag new file mode 100644 index 0000000..da1a19c --- /dev/null +++ b/src/primitives/shaders/shadowedbordertexture.frag @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#version 440 + +#extension GL_GOOGLE_include_directive: enable +#include "sdf.glsl" +// See sdf.glsl for the SDF related functions. + +// This shader renders a rectangle with rounded corners and a shadow below it. +// In addition it renders a border around it. + +#include "uniforms.glsl" +layout(binding = 1) uniform sampler2D textureSource; + +layout(location = 0) in lowp vec2 uv; +layout(location = 0) out lowp vec4 out_color; + +const lowp float minimum_shadow_radius = 0.05; + +void main() +{ + // Scaling factor that is the inverse of the amount of scaling applied to the geometry. + lowp float inverse_scale = 1.0 / (1.0 + ubuf.size + length(ubuf.offset) * 2.0); + + // Correction factor to round the corners of a larger shadow. + // We want to account for size in regards to shadow radius, so that a larger shadow is + // more rounded, but only if we are not already rounding the corners due to corner radius. + lowp vec4 size_factor = 0.5 * (minimum_shadow_radius / max(ubuf.radius, minimum_shadow_radius)); + lowp vec4 shadow_radius = ubuf.radius + ubuf.size * size_factor; + + lowp vec4 col = vec4(0.0); + + // Calculate the shadow's distance field. + lowp float shadow = sdf_rounded_rectangle(uv - ubuf.offset * 2.0 * inverse_scale, ubuf.aspect * inverse_scale, shadow_radius * inverse_scale); + // Render it, interpolating the color over the distance. + col = mix(col, ubuf.shadowColor * sign(ubuf.size), 1.0 - smoothstep(-ubuf.size * 0.5, ubuf.size * 0.5, shadow)); + + // Scale corrected corner radius + lowp vec4 corner_radius = ubuf.radius * inverse_scale; + + // Calculate the outer rectangle distance field and render it. + lowp float outer_rect = sdf_rounded_rectangle(uv, ubuf.aspect * inverse_scale, corner_radius); + + col = sdf_render(outer_rect, col, ubuf.borderColor); + + // The inner rectangle distance field is the outer reduced by twice the border width. + lowp float inner_rect = outer_rect + (ubuf.borderWidth * inverse_scale) * 2.0; + + // Render the inner rectangle. + col = sdf_render(inner_rect, col, ubuf.color); + + // Sample the texture, then blend it on top of the background color. + lowp vec2 texture_uv = ((uv / ubuf.aspect) + (1.0 * inverse_scale)) / (2.0 * inverse_scale); + lowp vec4 texture_color = texture(textureSource, texture_uv); + col = sdf_render(inner_rect, col, texture_color, texture_color.a, sdf_default_smoothing); + + out_color = col * ubuf.opacity; +} diff --git a/src/primitives/shaders/shadowedbordertexture_lowpower.frag b/src/primitives/shaders/shadowedbordertexture_lowpower.frag new file mode 100644 index 0000000..ca79d7b --- /dev/null +++ b/src/primitives/shaders/shadowedbordertexture_lowpower.frag @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#version 440 + +#extension GL_GOOGLE_include_directive: enable +#include "sdf_lowpower.glsl" +// See sdf.glsl for the SDF related functions. + +// This shader renders a rectangle with rounded corners and a shadow below it. +// In addition it renders a border around it. + +#include "uniforms.glsl" +layout(binding = 1) uniform sampler2D textureSource; + +layout(location = 0) in lowp vec2 uv; +layout(location = 0) out lowp vec4 out_color; + +const lowp float minimum_shadow_radius = 0.05; + +void main() +{ + lowp vec4 col = vec4(0.0); + + // Calculate the outer rectangle distance field. + lowp float outer_rect = sdf_rounded_rectangle(uv, ubuf.aspect, ubuf.radius); + + // Render it + col = sdf_render(outer_rect, col, ubuf.borderColor); + + // Inner rectangle distance field equals outer reduced by twice the border width + lowp float inner_rect = outer_rect + ubuf.borderWidth * 2.0; + + // Render it so we have a background for the image. + col = sdf_render(inner_rect, col, ubuf.color); + + // Sample the texture, then render it, blending with the background color. + lowp vec2 texture_uv = ((uv / ubuf.aspect) + 1.0) / 2.0; + lowp vec4 texture_color = texture(textureSource, texture_uv); + col = sdf_render(inner_rect, col, texture_color, texture_color.a, sdf_default_smoothing); + + out_color = col * ubuf.opacity; +} diff --git a/src/primitives/shaders/shadowedrectangle.frag b/src/primitives/shaders/shadowedrectangle.frag new file mode 100644 index 0000000..6705d45 --- /dev/null +++ b/src/primitives/shaders/shadowedrectangle.frag @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#version 440 + +#extension GL_GOOGLE_include_directive: enable +#include "sdf.glsl" +// See sdf.glsl for the SDF related functions. + +// This shader renders a rectangle with rounded corners and a shadow below it. + +#include "uniforms.glsl" + +layout(location = 0) in lowp vec2 uv; +layout(location = 0) out lowp vec4 out_color; + +const lowp float minimum_shadow_radius = 0.05; + +void main() +{ + // Scaling factor that is the inverse of the amount of scaling applied to the geometry. + lowp float inverse_scale = 1.0 / (1.0 + ubuf.size + length(ubuf.offset) * 2.0); + + // Correction factor to round the corners of a larger shadow. + // We want to account for size in regards to shadow radius, so that a larger shadow is + // more rounded, but only if we are not already rounding the corners due to corner radius. + lowp vec4 size_factor = 0.5 * (minimum_shadow_radius / max(ubuf.radius, minimum_shadow_radius)); + lowp vec4 shadow_radius = ubuf.radius + ubuf.size * size_factor; + + lowp vec4 col = vec4(0.0); + + // Calculate the shadow's distance field. + lowp float shadow = sdf_rounded_rectangle(uv - ubuf.offset * 2.0 * inverse_scale, ubuf.aspect * inverse_scale, shadow_radius * inverse_scale); + // Render it, interpolating the color over the distance. + col = mix(col, ubuf.shadowColor * sign(ubuf.size), 1.0 - smoothstep(-ubuf.size * 0.5, ubuf.size * 0.5, shadow)); + + // Calculate the main rectangle distance field and render it. + lowp float rect = sdf_rounded_rectangle(uv, ubuf.aspect * inverse_scale, ubuf.radius * inverse_scale); + + col = sdf_render(rect, col, ubuf.color); + + out_color = col * ubuf.opacity; +} diff --git a/src/primitives/shaders/shadowedrectangle.vert b/src/primitives/shaders/shadowedrectangle.vert new file mode 100644 index 0000000..312ed1b --- /dev/null +++ b/src/primitives/shaders/shadowedrectangle.vert @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#version 440 + +#extension GL_GOOGLE_include_directive: enable +#include "uniforms.glsl" + +layout(location = 0) in highp vec4 in_vertex; +layout(location = 1) in mediump vec2 in_uv; + +layout(location = 0) out mediump vec2 uv; + +out gl_PerVertex { vec4 gl_Position; }; + +void main() { + uv = (-1.0 + 2.0 * in_uv) * ubuf.aspect; + gl_Position = ubuf.matrix * in_vertex; +} diff --git a/src/primitives/shaders/shadowedrectangle_lowpower.frag b/src/primitives/shaders/shadowedrectangle_lowpower.frag new file mode 100644 index 0000000..8a88691 --- /dev/null +++ b/src/primitives/shaders/shadowedrectangle_lowpower.frag @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#version 440 + +// See sdf.glsl for the SDF related functions. +#extension GL_GOOGLE_include_directive: enable +#include "sdf_lowpower.glsl" + +// This is a version of shadowedrectangle.frag meant for very low power hardware +// (PinePhone). It does not render a shadow and does not do alpha blending. + +#include "uniforms.glsl" + +layout(location = 0) in lowp vec2 uv; +layout(location = 0) out lowp vec4 out_color; + +void main() +{ + lowp vec4 col = vec4(0.0); + + // Calculate the main rectangle distance field. + lowp float rect = sdf_rounded_rectangle(uv, ubuf.aspect, ubuf.radius); + + // Render it. + col = sdf_render(rect, col, ubuf.color); + + out_color = col * ubuf.opacity; +} diff --git a/src/primitives/shaders/shadowedtexture.frag b/src/primitives/shaders/shadowedtexture.frag new file mode 100644 index 0000000..508e3f9 --- /dev/null +++ b/src/primitives/shaders/shadowedtexture.frag @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#version 440 + +#extension GL_GOOGLE_include_directive: enable +#include "sdf.glsl" +// See sdf.glsl for the SDF related functions. + +// This shader renders a texture on top of a rectangle with rounded corners and +// a shadow below it. + +#include "uniforms.glsl" +layout(binding = 1) uniform sampler2D textureSource; + +layout(location = 0) in lowp vec2 uv; +layout(location = 0) out lowp vec4 out_color; + +const lowp float minimum_shadow_radius = 0.05; + +void main() +{ + // Scaling factor that is the inverse of the amount of scaling applied to the geometry. + lowp float inverse_scale = 1.0 / (1.0 + ubuf.size + length(ubuf.offset) * 2.0); + + // Correction factor to round the corners of a larger shadow. + // We want to account for size in regards to shadow radius, so that a larger shadow is + // more rounded, but only if we are not already rounding the corners due to corner radius. + lowp vec4 size_factor = 0.5 * (minimum_shadow_radius / max(ubuf.radius, minimum_shadow_radius)); + lowp vec4 shadow_radius = ubuf.radius + ubuf.size * size_factor; + + lowp vec4 col = vec4(0.0); + + // Calculate the shadow's distance field. + lowp float shadow = sdf_rounded_rectangle(uv - ubuf.offset * 2.0 * inverse_scale, ubuf.aspect * inverse_scale, shadow_radius * inverse_scale); + // Render it, interpolating the color over the distance. + col = mix(col, ubuf.shadowColor * sign(ubuf.size), 1.0 - smoothstep(-ubuf.size * 0.5, ubuf.size * 0.5, shadow)); + + // Calculate the main rectangle distance field and render it. + lowp float rect = sdf_rounded_rectangle(uv, ubuf.aspect * inverse_scale, ubuf.radius * inverse_scale); + + col = sdf_render(rect, col, ubuf.color); + + // Sample the texture, then blend it on top of the background color. + lowp vec2 texture_uv = ((uv / ubuf.aspect) + (1.0 * inverse_scale)) / (2.0 * inverse_scale); + lowp vec4 texture_color = texture(textureSource, texture_uv); + col = sdf_render(rect, col, texture_color, texture_color.a, sdf_default_smoothing); + + out_color = col * ubuf.opacity; +} diff --git a/src/primitives/shaders/shadowedtexture_lowpower.frag b/src/primitives/shaders/shadowedtexture_lowpower.frag new file mode 100644 index 0000000..e2b95c4 --- /dev/null +++ b/src/primitives/shaders/shadowedtexture_lowpower.frag @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#version 440 + +#extension GL_GOOGLE_include_directive: enable +#include "sdf_lowpower.glsl" +// See sdf.glsl for the SDF related functions. + +// This shader renders a texture on top of a rectangle with rounded corners and +// a shadow below it. + +#include "uniforms.glsl" +layout(binding = 1) uniform sampler2D textureSource; + +layout(location = 0) in lowp vec2 uv; +layout(location = 0) out lowp vec4 out_color; + +void main() +{ + lowp vec4 col = vec4(0.0); + + // Calculate the main rectangle distance field. + lowp float rect = sdf_rounded_rectangle(uv, ubuf.aspect, ubuf.radius); + + // Render it, so we have a background for the image. + col = sdf_render(rect, col, ubuf.color); + + // Sample the texture, then render it, blending it with the background. + lowp vec2 texture_uv = ((uv / ubuf.aspect) + 1.0) / 2.0; + lowp vec4 texture_color = texture(textureSource, texture_uv); + col = sdf_render(rect, col, texture_color, texture_color.a, sdf_default_smoothing); + + out_color = col * ubuf.opacity; +} diff --git a/src/primitives/shaders/uniforms.glsl b/src/primitives/shaders/uniforms.glsl new file mode 100644 index 0000000..0df5fb6 --- /dev/null +++ b/src/primitives/shaders/uniforms.glsl @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +layout(std140, binding = 0) uniform buf { + highp mat4 matrix; // offset 0 + lowp vec2 aspect; // offset 64 + + lowp float opacity; // offset 72 + lowp float size; // offset 76 + lowp vec4 radius; // offset 80 + lowp vec4 color; // offset 96 + lowp vec4 shadowColor; // offset 112 + lowp vec2 offset; // offset 128 + + lowp float borderWidth; // offset 136 + lowp vec4 borderColor; // offset 144 +} ubuf; // size 160 diff --git a/src/primitives/shadowedrectangle.cpp b/src/primitives/shadowedrectangle.cpp new file mode 100644 index 0000000..9845dab --- /dev/null +++ b/src/primitives/shadowedrectangle.cpp @@ -0,0 +1,365 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "shadowedrectangle.h" + +#include +#include +#include + +#include "scenegraph/paintedrectangleitem.h" +#include "scenegraph/shadowedrectanglenode.h" + +BorderGroup::BorderGroup(QObject *parent) + : QObject(parent) +{ +} + +qreal BorderGroup::width() const +{ + return m_width; +} + +void BorderGroup::setWidth(qreal newWidth) +{ + if (newWidth == m_width) { + return; + } + + m_width = newWidth; + Q_EMIT changed(); +} + +QColor BorderGroup::color() const +{ + return m_color; +} + +void BorderGroup::setColor(const QColor &newColor) +{ + if (newColor == m_color) { + return; + } + + m_color = newColor; + Q_EMIT changed(); +} + +ShadowGroup::ShadowGroup(QObject *parent) + : QObject(parent) +{ +} + +qreal ShadowGroup::size() const +{ + return m_size; +} + +void ShadowGroup::setSize(qreal newSize) +{ + if (newSize == m_size) { + return; + } + + m_size = newSize; + Q_EMIT changed(); +} + +qreal ShadowGroup::xOffset() const +{ + return m_xOffset; +} + +void ShadowGroup::setXOffset(qreal newXOffset) +{ + if (newXOffset == m_xOffset) { + return; + } + + m_xOffset = newXOffset; + Q_EMIT changed(); +} + +qreal ShadowGroup::yOffset() const +{ + return m_yOffset; +} + +void ShadowGroup::setYOffset(qreal newYOffset) +{ + if (newYOffset == m_yOffset) { + return; + } + + m_yOffset = newYOffset; + Q_EMIT changed(); +} + +QColor ShadowGroup::color() const +{ + return m_color; +} + +void ShadowGroup::setColor(const QColor &newColor) +{ + if (newColor == m_color) { + return; + } + + m_color = newColor; + Q_EMIT changed(); +} + +CornersGroup::CornersGroup(QObject *parent) + : QObject(parent) +{ +} + +qreal CornersGroup::topLeft() const +{ + return m_topLeft; +} + +void CornersGroup::setTopLeft(qreal newTopLeft) +{ + if (newTopLeft == m_topLeft) { + return; + } + + m_topLeft = newTopLeft; + Q_EMIT changed(); +} + +qreal CornersGroup::topRight() const +{ + return m_topRight; +} + +void CornersGroup::setTopRight(qreal newTopRight) +{ + if (newTopRight == m_topRight) { + return; + } + + m_topRight = newTopRight; + Q_EMIT changed(); +} + +qreal CornersGroup::bottomLeft() const +{ + return m_bottomLeft; +} + +void CornersGroup::setBottomLeft(qreal newBottomLeft) +{ + if (newBottomLeft == m_bottomLeft) { + return; + } + + m_bottomLeft = newBottomLeft; + Q_EMIT changed(); +} + +qreal CornersGroup::bottomRight() const +{ + return m_bottomRight; +} + +void CornersGroup::setBottomRight(qreal newBottomRight) +{ + if (newBottomRight == m_bottomRight) { + return; + } + + m_bottomRight = newBottomRight; + Q_EMIT changed(); +} + +QVector4D CornersGroup::toVector4D(float all) const +{ + return QVector4D{m_bottomRight < 0.0 ? all : m_bottomRight, + m_topRight < 0.0 ? all : m_topRight, + m_bottomLeft < 0.0 ? all : m_bottomLeft, + m_topLeft < 0.0 ? all : m_topLeft}; +} + +ShadowedRectangle::ShadowedRectangle(QQuickItem *parentItem) + : QQuickItem(parentItem) + , m_border(std::make_unique()) + , m_shadow(std::make_unique()) + , m_corners(std::make_unique()) +{ + setFlag(QQuickItem::ItemHasContents, true); + + connect(m_border.get(), &BorderGroup::changed, this, &ShadowedRectangle::update); + connect(m_shadow.get(), &ShadowGroup::changed, this, &ShadowedRectangle::update); + connect(m_corners.get(), &CornersGroup::changed, this, &ShadowedRectangle::update); +} + +ShadowedRectangle::~ShadowedRectangle() +{ +} + +BorderGroup *ShadowedRectangle::border() const +{ + return m_border.get(); +} + +ShadowGroup *ShadowedRectangle::shadow() const +{ + return m_shadow.get(); +} + +CornersGroup *ShadowedRectangle::corners() const +{ + return m_corners.get(); +} + +qreal ShadowedRectangle::radius() const +{ + return m_radius; +} + +void ShadowedRectangle::setRadius(qreal newRadius) +{ + if (newRadius == m_radius) { + return; + } + + m_radius = newRadius; + if (!isSoftwareRendering()) { + update(); + } + Q_EMIT radiusChanged(); +} + +QColor ShadowedRectangle::color() const +{ + return m_color; +} + +void ShadowedRectangle::setColor(const QColor &newColor) +{ + if (newColor == m_color) { + return; + } + + m_color = newColor; + if (!isSoftwareRendering()) { + update(); + } + Q_EMIT colorChanged(); +} + +ShadowedRectangle::RenderType ShadowedRectangle::renderType() const +{ + return m_renderType; +} + +void ShadowedRectangle::setRenderType(RenderType renderType) +{ + if (renderType == m_renderType) { + return; + } + m_renderType = renderType; + update(); + Q_EMIT renderTypeChanged(); +} + +void ShadowedRectangle::componentComplete() +{ + QQuickItem::componentComplete(); + + checkSoftwareItem(); +} + +bool ShadowedRectangle::isSoftwareRendering() const +{ + return (window() && window()->rendererInterface()->graphicsApi() == QSGRendererInterface::Software) || m_renderType == RenderType::Software; +} + +PaintedRectangleItem *ShadowedRectangle::softwareItem() const +{ + return m_softwareItem; +} + +void ShadowedRectangle::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) +{ + if (change == QQuickItem::ItemSceneChange && value.window) { + checkSoftwareItem(); + // TODO: only conditionally emit? + Q_EMIT softwareRenderingChanged(); + } + + QQuickItem::itemChange(change, value); +} + +QSGNode *ShadowedRectangle::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data) +{ + Q_UNUSED(data); + + if (boundingRect().isEmpty()) { + delete node; + return nullptr; + } + + auto shadowNode = static_cast(node); + + if (!shadowNode) { + shadowNode = new ShadowedRectangleNode{}; + + // Cache lowPower state so we only execute the full check once. + static bool lowPower = QByteArrayList{"1", "true"}.contains(qgetenv("KIRIGAMI_LOWPOWER_HARDWARE").toLower()); + if (m_renderType == RenderType::LowQuality || (m_renderType == RenderType::Auto && lowPower)) { + shadowNode->setShaderType(ShadowedRectangleMaterial::ShaderType::LowPower); + } + } + + shadowNode->setBorderEnabled(m_border->isEnabled()); + shadowNode->setRect(boundingRect()); + shadowNode->setSize(m_shadow->size()); + shadowNode->setRadius(m_corners->toVector4D(m_radius)); + shadowNode->setOffset(QVector2D{float(m_shadow->xOffset()), float(m_shadow->yOffset())}); + shadowNode->setColor(m_color); + shadowNode->setShadowColor(m_shadow->color()); + shadowNode->setBorderWidth(m_border->width()); + shadowNode->setBorderColor(m_border->color()); + shadowNode->updateGeometry(); + return shadowNode; +} + +void ShadowedRectangle::checkSoftwareItem() +{ + if (!m_softwareItem && isSoftwareRendering()) { + m_softwareItem = new PaintedRectangleItem{this}; + // The software item is added as a "normal" child item, this means it + // will be part of the normal item sort order. Since there is no way to + // control the ordering of children, just make sure to have a very low Z + // value for the child, to force it to be the lowest item. + m_softwareItem->setZ(-99.0); + + auto updateItem = [this]() { + auto borderWidth = m_border->width(); + auto rect = boundingRect(); + m_softwareItem->setSize(rect.size()); + m_softwareItem->setColor(m_color); + m_softwareItem->setRadius(m_radius); + m_softwareItem->setBorderWidth(borderWidth); + m_softwareItem->setBorderColor(m_border->color()); + }; + + updateItem(); + + connect(this, &ShadowedRectangle::widthChanged, m_softwareItem, updateItem); + connect(this, &ShadowedRectangle::heightChanged, m_softwareItem, updateItem); + connect(this, &ShadowedRectangle::colorChanged, m_softwareItem, updateItem); + connect(this, &ShadowedRectangle::radiusChanged, m_softwareItem, updateItem); + connect(m_border.get(), &BorderGroup::changed, m_softwareItem, updateItem); + setFlag(QQuickItem::ItemHasContents, false); + } +} + +#include "moc_shadowedrectangle.cpp" diff --git a/src/primitives/shadowedrectangle.h b/src/primitives/shadowedrectangle.h new file mode 100644 index 0000000..ed1d881 --- /dev/null +++ b/src/primitives/shadowedrectangle.h @@ -0,0 +1,370 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include + +#include + +class PaintedRectangleItem; + +/** + * @brief Grouped property for rectangle border. + */ +class BorderGroup : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + /** + * @brief This property holds the border's width in pixels. + * + * default: ``0``px + */ + Q_PROPERTY(qreal width READ width WRITE setWidth NOTIFY changed FINAL) + /** + * @brief This property holds the border's color. + * + * Full RGBA colors are supported. + * + * default: ``Qt::black`` + */ + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY changed FINAL) + +public: + explicit BorderGroup(QObject *parent = nullptr); + + qreal width() const; + void setWidth(qreal newWidth); + + QColor color() const; + void setColor(const QColor &newColor); + + Q_SIGNAL void changed(); + + inline bool isEnabled() const + { + return !qFuzzyIsNull(m_width); + } + +private: + qreal m_width = 0.0; + QColor m_color = Qt::black; +}; + +/** + * @brief Grouped property for the rectangle's shadow. + */ +class ShadowGroup : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + /** + * @brief This property holds the shadow's approximate size in pixels. + * @note The actual shadow size can be less than this value due to falloff. + * + * default: ``0``px + */ + Q_PROPERTY(qreal size READ size WRITE setSize NOTIFY changed FINAL) + /** + * @brief This property holds the shadow's offset in pixels on the X axis. + * + * default: ``0``px + */ + Q_PROPERTY(qreal xOffset READ xOffset WRITE setXOffset NOTIFY changed FINAL) + /** + * @brief This property holds the shadow's offset in pixels on the Y axis. + * + * default: ``0``px + */ + Q_PROPERTY(qreal yOffset READ yOffset WRITE setYOffset NOTIFY changed FINAL) + /** + * @brief This property holds the shadow's color. + * + * Full RGBA colors are supported. + * + * default: ``Qt::black`` + */ + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY changed FINAL) + +public: + explicit ShadowGroup(QObject *parent = nullptr); + + qreal size() const; + void setSize(qreal newSize); + + qreal xOffset() const; + void setXOffset(qreal newXOffset); + + qreal yOffset() const; + void setYOffset(qreal newYOffset); + + QColor color() const; + void setColor(const QColor &newShadowColor); + + Q_SIGNAL void changed(); + +private: + qreal m_size = 0.0; + qreal m_xOffset = 0.0; + qreal m_yOffset = 0.0; + QColor m_color = Qt::black; +}; + +/** + * @brief Grouped property for corner radius. + */ +class CornersGroup : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + /** + * @brief This property holds the top-left corner's radius in pixels. + * + * Setting this to ``-1`` indicates that the value should be ignored. + * + * default: ``-1``px + */ + Q_PROPERTY(qreal topLeftRadius READ topLeft WRITE setTopLeft NOTIFY changed FINAL) + + /** + * @brief This property holds the top-right corner's radius in pixels. + * + * Setting this to ``-1`` indicates that the value should be ignored. + * + * default: ``-1``px + */ + Q_PROPERTY(qreal topRightRadius READ topRight WRITE setTopRight NOTIFY changed FINAL) + + /** + * @brief This property holds the bottom-left corner's radius in pixels. + * + * Setting this to ``-1`` indicates that the value should be ignored. + * + * default: ``-1``px + */ + Q_PROPERTY(qreal bottomLeftRadius READ bottomLeft WRITE setBottomLeft NOTIFY changed FINAL) + + /** + * @brief This property holds the bottom-right corner's radius in pixels. + * + * Setting this to ``-1`` indicates that the value should be ignored. + * + * default: ``-1``px + */ + Q_PROPERTY(qreal bottomRightRadius READ bottomRight WRITE setBottomRight NOTIFY changed FINAL) + +public: + explicit CornersGroup(QObject *parent = nullptr); + + qreal topLeft() const; + void setTopLeft(qreal newTopLeft); + + qreal topRight() const; + void setTopRight(qreal newTopRight); + + qreal bottomLeft() const; + void setBottomLeft(qreal newBottomLeft); + + qreal bottomRight() const; + void setBottomRight(qreal newBottomRight); + + Q_SIGNAL void changed(); + + QVector4D toVector4D(float all) const; + +private: + float m_topLeft = -1.0; + float m_topRight = -1.0; + float m_bottomLeft = -1.0; + float m_bottomRight = -1.0; +}; + +/** + * @brief A rectangle with a shadow behind it. + * + * This item will render a rectangle, with a shadow below it. The rendering is done + * using distance fields, which provide greatly improved performance. The shadow is + * rendered outside of the item's bounds, so the item's width and height are the + * rectangle's width and height. + * + * @since 5.69 + * @since 2.12 + */ +class ShadowedRectangle : public QQuickItem +{ + Q_OBJECT + QML_ELEMENT + /** + * @brief This property holds the radii of the rectangle's corners. + * + * This is the amount of rounding to apply to all of the rectangle's + * corners, in pixels. Each corner can have a different radius. + * + * default: ``0`` + * + * @see corners + */ + Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged FINAL) + + /** + * @brief This property holds the rectangle's color. + * + * Full RGBA colors are supported. + * + * default: ``Qt::white`` + */ + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged FINAL) + + /** + * @brief This property holds the border's grouped property. + * + * Example usage: + * @code + * Kirigami.ShadowedRectangle { + * border.width: 2 + * border.color: Kirigami.Theme.textColor + * } + * @endcode + * @see BorderGroup + */ + Q_PROPERTY(BorderGroup *border READ border CONSTANT FINAL) + + /** + * @brief This property holds the shadow's grouped property. + * + * Example usage: + * @code + * Kirigami.ShadowedRectangle { + * shadow.size: 20 + * shadow.xOffset: 5 + * shadow.yOffset: 5 + * } + * @endcode + * + * @see ShadowGroup + */ + Q_PROPERTY(ShadowGroup *shadow READ shadow CONSTANT FINAL) + + /** + * @brief This property holds the corners grouped property + * + * Note that the values from this group override \property radius for the + * corner they affect. + * + * Example usage: + * @code + * Kirigami.ShadowedRectangle { + * corners.topLeftRadius: 4 + * corners.topRightRadius: 5 + * corners.bottomLeftRadius: 2 + * corners.bottomRightRadius: 10 + * @endcode + * + * @see CornersGroup + */ + Q_PROPERTY(CornersGroup *corners READ corners CONSTANT FINAL) + + /** + * @brief This property holds the rectangle's render mode. + * + * default: ``RenderType::Auto`` + * + * @see RenderType + */ + Q_PROPERTY(RenderType renderType READ renderType WRITE setRenderType NOTIFY renderTypeChanged FINAL) + + /** + * @brief This property tells whether software rendering is being used. + * + * default: ``false`` + */ + Q_PROPERTY(bool softwareRendering READ isSoftwareRendering NOTIFY softwareRenderingChanged FINAL) + +public: + ShadowedRectangle(QQuickItem *parent = nullptr); + ~ShadowedRectangle() override; + + /** + * @brief Available rendering types for ShadowedRectangle. + */ + enum RenderType { + /** + * @brief Automatically determine the optimal rendering type. + * + * This will use the highest rendering quality possible, falling back to + * lower quality if the hardware doesn't support it. It will use software + * rendering if the QtQuick scene graph is set to use software rendering. + */ + Auto, + + /** + * @brief Use the highest rendering quality possible, even if the hardware might + * not be able to handle it normally. + */ + HighQuality, + + /** + * @brief Use the lowest rendering quality, even if the hardware could handle + * higher quality rendering. + * + * This might result in certain effects being omitted, like shadows. + */ + LowQuality, + + /** + * @brief Always use software rendering for this rectangle. + * + * Software rendering is intended as a fallback when the QtQuick scene + * graph is configured to use software rendering. It will result in + * a number of missing features, like shadows and multiple corner radii. + */ + Software + }; + Q_ENUM(RenderType) + + BorderGroup *border() const; + ShadowGroup *shadow() const; + CornersGroup *corners() const; + + qreal radius() const; + void setRadius(qreal newRadius); + Q_SIGNAL void radiusChanged(); + + QColor color() const; + void setColor(const QColor &newColor); + Q_SIGNAL void colorChanged(); + + RenderType renderType() const; + void setRenderType(RenderType renderType); + Q_SIGNAL void renderTypeChanged(); + + void componentComplete() override; + + bool isSoftwareRendering() const; + +Q_SIGNALS: + void softwareRenderingChanged(); + +protected: + PaintedRectangleItem *softwareItem() const; + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) override; + QSGNode *updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data) override; + +private: + void checkSoftwareItem(); + const std::unique_ptr m_border; + const std::unique_ptr m_shadow; + const std::unique_ptr m_corners; + qreal m_radius = 0.0; + QColor m_color = Qt::white; + RenderType m_renderType = RenderType::Auto; + PaintedRectangleItem *m_softwareItem = nullptr; +}; diff --git a/src/primitives/shadowedtexture.cpp b/src/primitives/shadowedtexture.cpp new file mode 100644 index 0000000..b38c4d8 --- /dev/null +++ b/src/primitives/shadowedtexture.cpp @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "shadowedtexture.h" + +#include +#include +#include + +#include "scenegraph/shadowedtexturenode.h" + +ShadowedTexture::ShadowedTexture(QQuickItem *parentItem) + : ShadowedRectangle(parentItem) +{ +} + +ShadowedTexture::~ShadowedTexture() +{ +} + +QQuickItem *ShadowedTexture::source() const +{ + return m_source; +} + +void ShadowedTexture::setSource(QQuickItem *newSource) +{ + if (newSource == m_source) { + return; + } + + m_source = newSource; + m_sourceChanged = true; + if (m_source && !m_source->parentItem()) { + m_source->setParentItem(this); + } + + if (!isSoftwareRendering()) { + update(); + } + Q_EMIT sourceChanged(); +} + +QSGNode *ShadowedTexture::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data) +{ + Q_UNUSED(data) + + if (boundingRect().isEmpty()) { + delete node; + return nullptr; + } + + auto shadowNode = static_cast(node); + + if (!shadowNode || m_sourceChanged) { + m_sourceChanged = false; + delete shadowNode; + if (m_source) { + shadowNode = new ShadowedTextureNode{}; + } else { + shadowNode = new ShadowedRectangleNode{}; + } + + if (qEnvironmentVariableIsSet("KIRIGAMI_LOWPOWER_HARDWARE")) { + shadowNode->setShaderType(ShadowedRectangleMaterial::ShaderType::LowPower); + } + } + + shadowNode->setBorderEnabled(border()->isEnabled()); + shadowNode->setRect(boundingRect()); + shadowNode->setSize(shadow()->size()); + shadowNode->setRadius(corners()->toVector4D(radius())); + shadowNode->setOffset(QVector2D{float(shadow()->xOffset()), float(shadow()->yOffset())}); + shadowNode->setColor(color()); + shadowNode->setShadowColor(shadow()->color()); + shadowNode->setBorderWidth(border()->width()); + shadowNode->setBorderColor(border()->color()); + + if (m_source) { + static_cast(shadowNode)->setTextureSource(m_source->textureProvider()); + } + + shadowNode->updateGeometry(); + return shadowNode; +} + +#include "moc_shadowedtexture.cpp" diff --git a/src/primitives/shadowedtexture.h b/src/primitives/shadowedtexture.h new file mode 100644 index 0000000..3979804 --- /dev/null +++ b/src/primitives/shadowedtexture.h @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include "shadowedrectangle.h" + +/** + * A rectangle with a shadow, using a QQuickItem as texture. + * + * This item will render a source item, with a shadow below it. The rendering is done + * using distance fields, which provide greatly improved performance. The shadow is + * rendered outside of the item's bounds, so the item's width and height are the + * rectangle's width and height. + * + * @since 5.69 / 2.12 + */ +class ShadowedTexture : public ShadowedRectangle +{ + Q_OBJECT + QML_ELEMENT + + /** + * This property holds the source item that will get rendered with the + * shadow. + */ + Q_PROPERTY(QQuickItem *source READ source WRITE setSource NOTIFY sourceChanged FINAL) + +public: + ShadowedTexture(QQuickItem *parent = nullptr); + ~ShadowedTexture() override; + + QQuickItem *source() const; + void setSource(QQuickItem *newSource); + Q_SIGNAL void sourceChanged(); + +protected: + QSGNode *updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data) override; + +private: + QQuickItem *m_source = nullptr; + bool m_sourceChanged = false; +}; diff --git a/src/scenepositionattached.cpp b/src/scenepositionattached.cpp new file mode 100644 index 0000000..e77f16c --- /dev/null +++ b/src/scenepositionattached.cpp @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * SPDX-FileCopyrightText: 2023 ivan tkachenko + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "scenepositionattached.h" +#include +#include + +ScenePositionAttached::ScenePositionAttached(QObject *parent) + : QObject(parent) +{ + m_item = qobject_cast(parent); + connectAncestors(m_item); +} + +ScenePositionAttached::~ScenePositionAttached() +{ +} + +qreal ScenePositionAttached::x() const +{ + qreal x = 0.0; + QQuickItem *item = m_item; + + while (item) { + x += item->x(); + item = item->parentItem(); + } + + return x; +} + +qreal ScenePositionAttached::y() const +{ + qreal y = 0.0; + QQuickItem *item = m_item; + + while (item) { + y += item->y(); + item = item->parentItem(); + } + + return y; +} + +void ScenePositionAttached::connectAncestors(QQuickItem *item) +{ + if (!item) { + return; + } + + QQuickItem *ancestor = item; + while (ancestor) { + m_ancestors << ancestor; + + connect(ancestor, &QQuickItem::xChanged, this, &ScenePositionAttached::xChanged); + connect(ancestor, &QQuickItem::yChanged, this, &ScenePositionAttached::yChanged); + connect(ancestor, &QQuickItem::parentChanged, this, [this, ancestor]() { + while (!m_ancestors.isEmpty()) { + QQuickItem *last = m_ancestors.takeLast(); + // Disconnect the item which had its parent changed too, + // because connectAncestors() would reconnect it next. + disconnect(last, nullptr, this, nullptr); + if (last == ancestor) { + break; + } + } + + connectAncestors(ancestor); + + Q_EMIT xChanged(); + Q_EMIT yChanged(); + }); + + ancestor = ancestor->parentItem(); + } +} + +ScenePositionAttached *ScenePositionAttached::qmlAttachedProperties(QObject *object) +{ + return new ScenePositionAttached(object); +} + +#include "moc_scenepositionattached.cpp" diff --git a/src/scenepositionattached.h b/src/scenepositionattached.h new file mode 100644 index 0000000..afef25c --- /dev/null +++ b/src/scenepositionattached.h @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef SCENEPOSITIONATTACHED_H +#define SCENEPOSITIONATTACHED_H + +#include +#include + +class QQuickItem; + +/** + * This attached property contains the information about the scene position of the item: + * Its global x and y coordinates will update automatically and can be binded + * @code + * import org.kde.kirigami as Kirigami + * Text { + * text: ScenePosition.x + * } + * @endcode + * @since 2.3 + */ +class ScenePositionAttached : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_ATTACHED(ScenePositionAttached) + QML_NAMED_ELEMENT(ScenePosition) + QML_UNCREATABLE("") + /** + * The global scene X position + */ + Q_PROPERTY(qreal x READ x NOTIFY xChanged FINAL) + + /** + * The global scene Y position + */ + Q_PROPERTY(qreal y READ y NOTIFY yChanged FINAL) + +public: + explicit ScenePositionAttached(QObject *parent = nullptr); + ~ScenePositionAttached() override; + + qreal x() const; + qreal y() const; + + // QML attached property + static ScenePositionAttached *qmlAttachedProperties(QObject *object); + +Q_SIGNALS: + void xChanged(); + void yChanged(); + +private: + void connectAncestors(QQuickItem *item); + + QQuickItem *m_item = nullptr; + QList m_ancestors; +}; + +QML_DECLARE_TYPEINFO(ScenePositionAttached, QML_HAS_ATTACHED_PROPERTIES) + +#endif // SCENEPOSITIONATTACHED_H diff --git a/src/spellcheckattached.cpp b/src/spellcheckattached.cpp new file mode 100644 index 0000000..c59b05c --- /dev/null +++ b/src/spellcheckattached.cpp @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "spellcheckattached.h" +#include + +SpellCheckAttached::SpellCheckAttached(QObject *parent) + : QObject(parent) +{ +} + +SpellCheckAttached::~SpellCheckAttached() +{ +} + +void SpellCheckAttached::setEnabled(bool enabled) +{ + if (enabled == m_enabled) { + return; + } + + m_enabled = enabled; + Q_EMIT enabledChanged(); +} + +bool SpellCheckAttached::enabled() const +{ + return m_enabled; +} + +SpellCheckAttached *SpellCheckAttached::qmlAttachedProperties(QObject *object) +{ + return new SpellCheckAttached(object); +} + +#include "moc_spellcheckattached.cpp" diff --git a/src/spellcheckattached.h b/src/spellcheckattached.h new file mode 100644 index 0000000..fdfa8b6 --- /dev/null +++ b/src/spellcheckattached.h @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.0-or-later + +#ifndef SPELLCHECKATTACHED_H +#define SPELLCHECKATTACHED_H + +#include +#include + +#include + +/** + * @brief This attached property contains hints for spell checker. + * + * @warning Kirigami doesn't provide any spell checker per se, this is just a + * hint for QQC2 style implementation and other downstream components. If you + * want to add spell checking to your custom application theme checkout + * \ref Sonnet. + * + * @code + * import QtQuick.Controls as QQC2 + * import org.kde.kirigami as Kirigami + * + * QQC2.TextArea { + * Kirigami.SpellCheck.enabled: true + * } + * @endcode + * + * @author Carl Schwan + * @since 2.18 + */ +class SpellCheckAttached : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_NAMED_ELEMENT(SpellCheck) + QML_UNCREATABLE("Attached property only") + QML_ATTACHED(SpellCheckAttached) + + /** + * This property holds whether the spell checking should be enabled on the + * TextField/TextArea. + * + * @note By default, false. This might change in KF6, so if you don't want + * spellcheck in your application, explicitly set it to false. + * + * @since 2.18 + */ + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged FINAL) +public: + explicit SpellCheckAttached(QObject *parent = nullptr); + ~SpellCheckAttached() override; + + void setEnabled(bool enabled); + bool enabled() const; + + // QML attached property + static SpellCheckAttached *qmlAttachedProperties(QObject *object); + +Q_SIGNALS: + void enabledChanged(); + +private: + bool m_enabled = false; +}; + +QML_DECLARE_TYPEINFO(SpellCheckAttached, QML_HAS_ATTACHED_PROPERTIES) + +#endif // SPELLCHECKATTACHED_H diff --git a/src/styles/Material/InlineMessage.qml b/src/styles/Material/InlineMessage.qml new file mode 100644 index 0000000..6bb28fa --- /dev/null +++ b/src/styles/Material/InlineMessage.qml @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2018 Eike Hein + * SPDX-FileCopyrightText: 2018 Marco Martin + * SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami +import org.kde.kirigami.templates as KT + +KT.InlineMessage { + id: root + + // a rectangle padded with anchors.margins is used to simulate a border + padding: bgFillRect.anchors.margins + Kirigami.Units.smallSpacing + + background: Kirigami.ShadowedRectangle { + id: bgBorderRect + + color: switch (root.type) { + case Kirigami.MessageType.Positive: return Kirigami.Theme.positiveTextColor; + case Kirigami.MessageType.Warning: return Kirigami.Theme.neutralTextColor; + case Kirigami.MessageType.Error: return Kirigami.Theme.negativeTextColor; + default: return Kirigami.Theme.activeTextColor; + } + + radius: Kirigami.Units.cornerRadius + shadow.size: 12 + shadow.xOffset: 0 + shadow.yOffset: 1 + shadow.color: Qt.rgba(0, 0, 0, 0.5) + + Rectangle { + id: bgFillRect + + anchors.fill: parent + anchors.margins: 1 + + color: Kirigami.Theme.backgroundColor + + radius: bgBorderRect.radius + } + + Rectangle { + anchors.fill: bgFillRect + + color: bgBorderRect.color + + opacity: 0.20 + + radius: bgFillRect.radius + } + } +} diff --git a/src/styles/Material/Theme.qml b/src/styles/Material/Theme.qml new file mode 100644 index 0000000..6433f86 --- /dev/null +++ b/src/styles/Material/Theme.qml @@ -0,0 +1,96 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls.Material +import org.kde.kirigami as Kirigami + +Kirigami.BasicThemeDefinition { + textColor: Material.foreground + disabledTextColor: Qt.alpha(Material.foreground, 0.6) + + highlightColor: Material.accent + //FIXME: something better? + highlightedTextColor: Material.background + backgroundColor: Material.background + alternateBackgroundColor: Qt.darker(Material.background, 1.05) + + hoverColor: Material.buttonColor(Material.theme, Material.background, Material.accent, true, false, true, false) + focusColor: Material.buttonColor(Material.theme, Material.background, Material.accent, true, false, true, false) + + activeTextColor: Material.primary + activeBackgroundColor: Material.primary + linkColor: "#2980B9" + linkBackgroundColor: "#2980B9" + visitedLinkColor: "#7F8C8D" + visitedLinkBackgroundColor: "#7F8C8D" + negativeTextColor: "#DA4453" + negativeBackgroundColor: "#DA4453" + neutralTextColor: "#F67400" + neutralBackgroundColor: "#F67400" + positiveTextColor: "#27AE60" + positiveBackgroundColor: "#27AE60" + + buttonTextColor: Material.foreground + buttonBackgroundColor: Material.buttonColor(Material.theme, Material.background, Material.accent, true, false, false, false) + buttonAlternateBackgroundColor: Qt.darker(Material.buttonColor, 1.05) + buttonHoverColor: Material.buttonColor(Material.theme, Material.background, Material.accent, true, false, true, false) + buttonFocusColor: Material.buttonColor(Material.theme, Material.background, Material.accent, true, false, true, false) + + viewTextColor: Material.foreground + viewBackgroundColor: Material.dialogColor + viewAlternateBackgroundColor: Qt.darker(Material.dialogColor, 1.05) + viewHoverColor: Material.listHighlightColor + viewFocusColor: Material.listHighlightColor + + selectionTextColor: Material.primaryHighlightedTextColor + selectionBackgroundColor: Material.textSelectionColor + selectionAlternateBackgroundColor: Qt.darker(Material.textSelectionColor, 1.05) + selectionHoverColor: Material.buttonColor(Material.theme, Material.background, Material.accent, true, false, true, false) + selectionFocusColor: Material.buttonColor(Material.theme, Material.background, Material.accent, true, false, true, false) + + tooltipTextColor: fontMetrics.Material.foreground + tooltipBackgroundColor: fontMetrics.Material.tooltipColor + tooltipAlternateBackgroundColor: Qt.darker(Material.tooltipColor, 1.05) + tooltipHoverColor: fontMetrics.Material.buttonColor(Material.theme, Material.background, Material.accent, true, false, true, false) + tooltipFocusColor: fontMetrics.Material.buttonColor(Material.theme, Material.background, Material.accent, true, false, true, false) + + complementaryTextColor: fontMetrics.Material.foreground + complementaryBackgroundColor: fontMetrics.Material.background + complementaryAlternateBackgroundColor: Qt.lighter(fontMetrics.Material.background, 1.05) + complementaryHoverColor: Material.buttonColor(Material.theme, Material.background, Material.accent, true, false, true, false) + complementaryFocusColor: Material.buttonColor(Material.theme, Material.background, Material.accent, true, false, true, false) + + headerTextColor: fontMetrics.Material.primaryTextColor + headerBackgroundColor: fontMetrics.Material.primaryColor + headerAlternateBackgroundColor: Qt.lighter(fontMetrics.Material.primaryColor, 1.05) + headerHoverColor: Material.buttonColor(Material.theme, Material.background, Material.accent, true, false, true, false) + headerFocusColor: Material.buttonColor(Material.theme, Material.background, Material.accent, true, false, true, false) + + defaultFont: fontMetrics.font + + property list children: [ + TextMetrics { + id: fontMetrics + //this is to get a source of dark colors + Material.theme: Material.Dark + } + ] + + onSync: object => { + //TODO: actually check if it's a dark or light color + if (object.Kirigami.Theme.colorSet === Kirigami.Theme.Complementary) { + object.Material.theme = Material.Dark + } else { + object.Material.theme = Material.Light + } + + object.Material.foreground = object.Kirigami.Theme.textColor + object.Material.background = object.Kirigami.Theme.backgroundColor + object.Material.primary = object.Kirigami.Theme.highlightColor + object.Material.accent = object.Kirigami.Theme.highlightColor + } +} diff --git a/src/styles/org.kde.desktop/AbstractApplicationHeader.qml b/src/styles/org.kde.desktop/AbstractApplicationHeader.qml new file mode 100644 index 0000000..fac2e34 --- /dev/null +++ b/src/styles/org.kde.desktop/AbstractApplicationHeader.qml @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami +import "../../templates" as T + +T.AbstractApplicationHeader { + id: root + + // Always use header bg color for toolbar (if available), even if the page + // it's located on uses a different color set + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Header + + background: Rectangle { + color: Kirigami.Theme.backgroundColor + Kirigami.Separator { + visible: root.separatorVisible && (!root.page || !root.page.header || !root.page.header.visible || root.page.header.toString().indexOf("ToolBar") === -1) + anchors { + left: parent.left + right: parent.right + bottom: root.y <= 0 ? parent.bottom : undefined + top: root.y <= 0 ? undefined : parent.top + } + } + } +} diff --git a/src/styles/org.kde.desktop/Theme.qml b/src/styles/org.kde.desktop/Theme.qml new file mode 100644 index 0000000..06dbf12 --- /dev/null +++ b/src/styles/org.kde.desktop/Theme.qml @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami + +Kirigami.BasicThemeDefinition { + textColor: palette.windowText + disabledTextColor: disabledPalette.windowText + + highlightColor: palette.highlight + highlightedTextColor: palette.highlightedText + backgroundColor: palette.window + alternateBackgroundColor: Qt.darker(palette.window, 1.05) + activeTextColor: palette.highlightedText + activeBackgroundColor: palette.highlight + linkColor: "#2980B9" + linkBackgroundColor: "#2980B9" + visitedLinkColor: "#7F8C8D" + visitedLinkBackgroundColor: "#7F8C8D" + hoverColor: palette.highlight + focusColor: palette.highlight + negativeTextColor: "#DA4453" + negativeBackgroundColor: "#DA4453" + neutralTextColor: "#F67400" + neutralBackgroundColor: "#F67400" + positiveTextColor: "#27AE60" + positiveBackgroundColor: "#27AE60" + + buttonTextColor: palette.buttonText + buttonBackgroundColor: palette.button + buttonAlternateBackgroundColor: Qt.darker(palette.button, 1.05) + buttonHoverColor: palette.highlight + buttonFocusColor: palette.highlight + + viewTextColor: palette.text + viewBackgroundColor: palette.base + viewAlternateBackgroundColor: palette.alternateBase + viewHoverColor: palette.highlight + viewFocusColor: palette.highlight + + selectionTextColor: palette.highlightedText + selectionBackgroundColor: palette.highlight + selectionAlternateBackgroundColor: Qt.darker(palette.highlight, 1.05) + selectionHoverColor: palette.highlight + selectionFocusColor: palette.highlight + + tooltipTextColor: palette.text + tooltipBackgroundColor: palette.base + tooltipAlternateBackgroundColor: palette.alternateBase + tooltipHoverColor: palette.highlight + tooltipFocusColor: palette.highlight + + complementaryTextColor: palette.text + complementaryBackgroundColor: palette.base + complementaryAlternateBackgroundColor: palette.alternateBase + complementaryHoverColor: palette.highlight + complementaryFocusColor: palette.highlight + + headerTextColor: palette.text + headerBackgroundColor: palette.base + headerAlternateBackgroundColor: palette.alternateBase + headerHoverColor: palette.highlight + headerFocusColor: palette.highlight + + defaultFont: fontMetrics.font + + property list children: [ + TextMetrics { + id: fontMetrics + }, + SystemPalette { + id: palette + colorGroup: SystemPalette.Active + }, + SystemPalette { + id: disabledPalette + colorGroup: SystemPalette.Disabled + } + ] + + function __propagateColorSet(object, context) {} + + function __propagateTextColor(object, color) {} + function __propagateBackgroundColor(object, color) {} + function __propagatePrimaryColor(object, color) {} + function __propagateAccentColor(object, color) {} +} diff --git a/src/wheelhandler.cpp b/src/wheelhandler.cpp new file mode 100644 index 0000000..f8734a1 --- /dev/null +++ b/src/wheelhandler.cpp @@ -0,0 +1,752 @@ +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "wheelhandler.h" +#include "settings.h" + +#include +#include +#include +#include + +KirigamiWheelEvent::KirigamiWheelEvent(QObject *parent) + : QObject(parent) +{ +} + +KirigamiWheelEvent::~KirigamiWheelEvent() +{ +} + +void KirigamiWheelEvent::initializeFromEvent(QWheelEvent *event) +{ + m_x = event->position().x(); + m_y = event->position().y(); + m_angleDelta = event->angleDelta(); + m_pixelDelta = event->pixelDelta(); + m_buttons = event->buttons(); + m_modifiers = event->modifiers(); + m_accepted = false; + m_inverted = event->inverted(); +} + +qreal KirigamiWheelEvent::x() const +{ + return m_x; +} + +qreal KirigamiWheelEvent::y() const +{ + return m_y; +} + +QPointF KirigamiWheelEvent::angleDelta() const +{ + return m_angleDelta; +} + +QPointF KirigamiWheelEvent::pixelDelta() const +{ + return m_pixelDelta; +} + +int KirigamiWheelEvent::buttons() const +{ + return m_buttons; +} + +int KirigamiWheelEvent::modifiers() const +{ + return m_modifiers; +} + +bool KirigamiWheelEvent::inverted() const +{ + return m_inverted; +} + +bool KirigamiWheelEvent::isAccepted() +{ + return m_accepted; +} + +void KirigamiWheelEvent::setAccepted(bool accepted) +{ + m_accepted = accepted; +} + +/////////////////////////////// + +WheelFilterItem::WheelFilterItem(QQuickItem *parent) + : QQuickItem(parent) +{ + setEnabled(false); +} + +/////////////////////////////// + +WheelHandler::WheelHandler(QObject *parent) + : QObject(parent) + , m_filterItem(new WheelFilterItem(nullptr)) +{ + m_filterItem->installEventFilter(this); + + m_wheelScrollingTimer.setSingleShot(true); + m_wheelScrollingTimer.setInterval(m_wheelScrollingDuration); + m_wheelScrollingTimer.callOnTimeout([this]() { + setScrolling(false); + }); + + m_yScrollAnimation.setEasingCurve(QEasingCurve::OutCubic); + + connect(QGuiApplication::styleHints(), &QStyleHints::wheelScrollLinesChanged, this, [this](int scrollLines) { + m_defaultPixelStepSize = 20 * scrollLines; + if (!m_explicitVStepSize && m_verticalStepSize != m_defaultPixelStepSize) { + m_verticalStepSize = m_defaultPixelStepSize; + Q_EMIT verticalStepSizeChanged(); + } + if (!m_explicitHStepSize && m_horizontalStepSize != m_defaultPixelStepSize) { + m_horizontalStepSize = m_defaultPixelStepSize; + Q_EMIT horizontalStepSizeChanged(); + } + }); +} + +WheelHandler::~WheelHandler() +{ + delete m_filterItem; +} + +QQuickItem *WheelHandler::target() const +{ + return m_flickable; +} + +void WheelHandler::setTarget(QQuickItem *target) +{ + if (m_flickable == target) { + return; + } + + if (target && !target->inherits("QQuickFlickable")) { + qmlWarning(this) << "target must be a QQuickFlickable"; + return; + } + + if (m_flickable) { + m_flickable->removeEventFilter(this); + disconnect(m_flickable, nullptr, m_filterItem, nullptr); + disconnect(m_flickable, &QQuickItem::parentChanged, this, &WheelHandler::_k_rebindScrollBars); + } + + m_flickable = target; + m_filterItem->setParentItem(target); + if (m_yScrollAnimation.targetObject()) { + m_yScrollAnimation.stop(); + } + m_yScrollAnimation.setTargetObject(target); + + if (target) { + target->installEventFilter(this); + + // Stack WheelFilterItem over the Flickable's scrollable content + m_filterItem->stackAfter(target->property("contentItem").value()); + // Make it fill the Flickable + m_filterItem->setWidth(target->width()); + m_filterItem->setHeight(target->height()); + connect(target, &QQuickItem::widthChanged, m_filterItem, [this, target]() { + m_filterItem->setWidth(target->width()); + }); + connect(target, &QQuickItem::heightChanged, m_filterItem, [this, target]() { + m_filterItem->setHeight(target->height()); + }); + } + + _k_rebindScrollBars(); + + Q_EMIT targetChanged(); +} + +void WheelHandler::_k_rebindScrollBars() +{ + struct ScrollBarAttached { + QObject *attached = nullptr; + QQuickItem *vertical = nullptr; + QQuickItem *horizontal = nullptr; + }; + + ScrollBarAttached attachedToFlickable; + ScrollBarAttached attachedToScrollView; + + if (m_flickable) { + // Get ScrollBars so that we can filter them too, even if they're not + // in the bounds of the Flickable + const auto flickableChildren = m_flickable->children(); + for (const auto child : flickableChildren) { + if (child->inherits("QQuickScrollBarAttached")) { + attachedToFlickable.attached = child; + attachedToFlickable.vertical = child->property("vertical").value(); + attachedToFlickable.horizontal = child->property("horizontal").value(); + break; + } + } + + // Check ScrollView if there are no scrollbars attached to the Flickable. + // We need to check if the parent inherits QQuickScrollView in case the + // parent is another Flickable that already has a Kirigami WheelHandler. + auto flickableParent = m_flickable->parentItem(); + if (flickableParent && flickableParent->inherits("QQuickScrollView")) { + const auto siblings = flickableParent->children(); + for (const auto child : siblings) { + if (child->inherits("QQuickScrollBarAttached")) { + attachedToScrollView.attached = child; + attachedToScrollView.vertical = child->property("vertical").value(); + attachedToScrollView.horizontal = child->property("horizontal").value(); + break; + } + } + } + } + + // Dilemma: ScrollBars can be attached to both ScrollView and Flickable, + // but only one of them should be shown anyway. Let's prefer Flickable. + + struct ChosenScrollBar { + QObject *attached = nullptr; + QQuickItem *scrollBar = nullptr; + }; + + ChosenScrollBar vertical; + if (attachedToFlickable.vertical) { + vertical.attached = attachedToFlickable.attached; + vertical.scrollBar = attachedToFlickable.vertical; + } else if (attachedToScrollView.vertical) { + vertical.attached = attachedToScrollView.attached; + vertical.scrollBar = attachedToScrollView.vertical; + } + + ChosenScrollBar horizontal; + if (attachedToFlickable.horizontal) { + horizontal.attached = attachedToFlickable.attached; + horizontal.scrollBar = attachedToFlickable.horizontal; + } else if (attachedToScrollView.horizontal) { + horizontal.attached = attachedToScrollView.attached; + horizontal.scrollBar = attachedToScrollView.horizontal; + } + + // Flickable may get re-parented to or out of a ScrollView, so we need to + // redo the discovery process. This is especially important for + // Kirigami.ScrollablePage component. + if (m_flickable) { + if (attachedToFlickable.horizontal && attachedToFlickable.vertical) { + // But if both scrollbars are already those from the preferred + // Flickable, there's no need for rediscovery. + disconnect(m_flickable, &QQuickItem::parentChanged, this, &WheelHandler::_k_rebindScrollBars); + } else { + connect(m_flickable, &QQuickItem::parentChanged, this, &WheelHandler::_k_rebindScrollBars, Qt::UniqueConnection); + } + } + + if (m_verticalScrollBar != vertical.scrollBar) { + if (m_verticalScrollBar) { + m_verticalScrollBar->removeEventFilter(this); + disconnect(m_verticalChangedConnection); + } + m_verticalScrollBar = vertical.scrollBar; + if (vertical.scrollBar) { + vertical.scrollBar->installEventFilter(this); + m_verticalChangedConnection = connect(vertical.attached, SIGNAL(verticalChanged()), this, SLOT(_k_rebindScrollBars())); + } + } + + if (m_horizontalScrollBar != horizontal.scrollBar) { + if (m_horizontalScrollBar) { + m_horizontalScrollBar->removeEventFilter(this); + disconnect(m_horizontalChangedConnection); + } + m_horizontalScrollBar = horizontal.scrollBar; + if (horizontal.scrollBar) { + horizontal.scrollBar->installEventFilter(this); + m_horizontalChangedConnection = connect(horizontal.attached, SIGNAL(horizontalChanged()), this, SLOT(_k_rebindScrollBars())); + } + } +} + +qreal WheelHandler::verticalStepSize() const +{ + return m_verticalStepSize; +} + +void WheelHandler::setVerticalStepSize(qreal stepSize) +{ + m_explicitVStepSize = true; + if (qFuzzyCompare(m_verticalStepSize, stepSize)) { + return; + } + // Mimic the behavior of QQuickScrollBar when stepSize is 0 + if (qFuzzyIsNull(stepSize)) { + resetVerticalStepSize(); + return; + } + m_verticalStepSize = stepSize; + Q_EMIT verticalStepSizeChanged(); +} + +void WheelHandler::resetVerticalStepSize() +{ + m_explicitVStepSize = false; + if (qFuzzyCompare(m_verticalStepSize, m_defaultPixelStepSize)) { + return; + } + m_verticalStepSize = m_defaultPixelStepSize; + Q_EMIT verticalStepSizeChanged(); +} + +qreal WheelHandler::horizontalStepSize() const +{ + return m_horizontalStepSize; +} + +void WheelHandler::setHorizontalStepSize(qreal stepSize) +{ + m_explicitHStepSize = true; + if (qFuzzyCompare(m_horizontalStepSize, stepSize)) { + return; + } + // Mimic the behavior of QQuickScrollBar when stepSize is 0 + if (qFuzzyIsNull(stepSize)) { + resetHorizontalStepSize(); + return; + } + m_horizontalStepSize = stepSize; + Q_EMIT horizontalStepSizeChanged(); +} + +void WheelHandler::resetHorizontalStepSize() +{ + m_explicitHStepSize = false; + if (qFuzzyCompare(m_horizontalStepSize, m_defaultPixelStepSize)) { + return; + } + m_horizontalStepSize = m_defaultPixelStepSize; + Q_EMIT horizontalStepSizeChanged(); +} + +Qt::KeyboardModifiers WheelHandler::pageScrollModifiers() const +{ + return m_pageScrollModifiers; +} + +void WheelHandler::setPageScrollModifiers(Qt::KeyboardModifiers modifiers) +{ + if (m_pageScrollModifiers == modifiers) { + return; + } + m_pageScrollModifiers = modifiers; + Q_EMIT pageScrollModifiersChanged(); +} + +void WheelHandler::resetPageScrollModifiers() +{ + setPageScrollModifiers(m_defaultPageScrollModifiers); +} + +bool WheelHandler::filterMouseEvents() const +{ + return m_filterMouseEvents; +} + +void WheelHandler::setFilterMouseEvents(bool enabled) +{ + if (m_filterMouseEvents == enabled) { + return; + } + m_filterMouseEvents = enabled; + Q_EMIT filterMouseEventsChanged(); +} + +bool WheelHandler::keyNavigationEnabled() const +{ + return m_keyNavigationEnabled; +} + +void WheelHandler::setKeyNavigationEnabled(bool enabled) +{ + if (m_keyNavigationEnabled == enabled) { + return; + } + m_keyNavigationEnabled = enabled; + Q_EMIT keyNavigationEnabledChanged(); +} + +void WheelHandler::classBegin() +{ + // Initializes smooth scrolling + m_engine = qmlEngine(this); + m_units = m_engine->singletonInstance("org.kde.kirigami.platform", "Units"); + m_settings = m_engine->singletonInstance("org.kde.kirigami.platform", "Settings"); + initSmoothScrollDuration(); + + connect(m_units, &Kirigami::Platform::Units::longDurationChanged, this, &WheelHandler::initSmoothScrollDuration); + connect(m_settings, &Kirigami::Platform::Settings::smoothScrollChanged, this, &WheelHandler::initSmoothScrollDuration); +} + +void WheelHandler::componentComplete() +{ +} + +void WheelHandler::initSmoothScrollDuration() +{ + if (m_settings->smoothScroll()) { + m_yScrollAnimation.setDuration(m_units->longDuration()); + } else { + m_yScrollAnimation.setDuration(0); + } +} + +void WheelHandler::setScrolling(bool scrolling) +{ + if (m_wheelScrolling == scrolling) { + if (m_wheelScrolling) { + m_wheelScrollingTimer.start(); + } + return; + } + m_wheelScrolling = scrolling; + m_filterItem->setEnabled(m_wheelScrolling); +} + +bool WheelHandler::scrollFlickable(QPointF pixelDelta, QPointF angleDelta, Qt::KeyboardModifiers modifiers) +{ + if (!m_flickable || (pixelDelta.isNull() && angleDelta.isNull())) { + return false; + } + + const qreal width = m_flickable->width(); + const qreal height = m_flickable->height(); + const qreal contentWidth = m_flickable->property("contentWidth").toReal(); + const qreal contentHeight = m_flickable->property("contentHeight").toReal(); + const qreal contentX = m_flickable->property("contentX").toReal(); + const qreal contentY = m_flickable->property("contentY").toReal(); + const qreal topMargin = m_flickable->property("topMargin").toReal(); + const qreal bottomMargin = m_flickable->property("bottomMargin").toReal(); + const qreal leftMargin = m_flickable->property("leftMargin").toReal(); + const qreal rightMargin = m_flickable->property("rightMargin").toReal(); + const qreal originX = m_flickable->property("originX").toReal(); + const qreal originY = m_flickable->property("originY").toReal(); + const qreal pageWidth = width - leftMargin - rightMargin; + const qreal pageHeight = height - topMargin - bottomMargin; + const auto window = m_flickable->window(); + const qreal devicePixelRatio = window != nullptr ? window->devicePixelRatio() : qGuiApp->devicePixelRatio(); + + // HACK: Only transpose deltas when not using xcb in order to not conflict with xcb's own delta transposing + if (modifiers & m_defaultHorizontalScrollModifiers && qGuiApp->platformName() != QLatin1String("xcb")) { + angleDelta = angleDelta.transposed(); + pixelDelta = pixelDelta.transposed(); + } + + const qreal xTicks = angleDelta.x() / 120; + const qreal yTicks = angleDelta.y() / 120; + qreal xChange; + qreal yChange; + bool scrolled = false; + + // Scroll X + if (contentWidth > pageWidth) { + // Use page size with pageScrollModifiers. Matches QScrollBar, which uses QAbstractSlider behavior. + if (modifiers & m_pageScrollModifiers) { + xChange = qBound(-pageWidth, xTicks * pageWidth, pageWidth); + } else if (pixelDelta.x() != 0) { + xChange = pixelDelta.x(); + } else { + xChange = xTicks * m_horizontalStepSize; + } + + // contentX and contentY use reversed signs from what x and y would normally use, so flip the signs + + qreal minXExtent = leftMargin - originX; + qreal maxXExtent = width - (contentWidth + rightMargin + originX); + + qreal newContentX = qBound(-minXExtent, contentX - xChange, -maxXExtent); + // Flickable::pixelAligned rounds the position, so round to mimic that behavior. + // Rounding prevents fractional positioning from causing text to be + // clipped off on the top and bottom. + // Multiply by devicePixelRatio before rounding and divide by devicePixelRatio + // after to make position match pixels on the screen more closely. + newContentX = std::round(newContentX * devicePixelRatio) / devicePixelRatio; + if (contentX != newContentX) { + scrolled = true; + m_flickable->setProperty("contentX", newContentX); + } + } + + // Scroll Y + if (contentHeight > pageHeight) { + if (modifiers & m_pageScrollModifiers) { + yChange = qBound(-pageHeight, yTicks * pageHeight, pageHeight); + } else if (pixelDelta.y() != 0) { + yChange = pixelDelta.y(); + } else { + yChange = yTicks * m_verticalStepSize; + } + + // contentX and contentY use reversed signs from what x and y would normally use, so flip the signs + + qreal minYExtent = topMargin - originY; + qreal maxYExtent = height - (contentHeight + bottomMargin + originY); + + qreal newContentY; + if (m_yScrollAnimation.state() == QPropertyAnimation::Running) { + m_yScrollAnimation.stop(); + newContentY = std::clamp(m_yScrollAnimation.endValue().toReal() + -yChange, -minYExtent, -maxYExtent); + } else { + newContentY = std::clamp(contentY - yChange, -minYExtent, -maxYExtent); + } + + // Flickable::pixelAligned rounds the position, so round to mimic that behavior. + // Rounding prevents fractional positioning from causing text to be + // clipped off on the top and bottom. + // Multiply by devicePixelRatio before rounding and divide by devicePixelRatio + // after to make position match pixels on the screen more closely. + newContentY = std::round(newContentY * devicePixelRatio) / devicePixelRatio; + if (contentY != newContentY) { + scrolled = true; + if (m_wasTouched || !m_engine) { + m_flickable->setProperty("contentY", newContentY); + } else { + m_yScrollAnimation.setEndValue(newContentY); + m_yScrollAnimation.start(QAbstractAnimation::KeepWhenStopped); + } + } + } + + return scrolled; +} + +bool WheelHandler::scrollUp(qreal stepSize) +{ + if (qFuzzyIsNull(stepSize)) { + return false; + } else if (stepSize < 0) { + stepSize = m_verticalStepSize; + } + // contentY uses reversed sign + return scrollFlickable(QPointF(0, stepSize)); +} + +bool WheelHandler::scrollDown(qreal stepSize) +{ + if (qFuzzyIsNull(stepSize)) { + return false; + } else if (stepSize < 0) { + stepSize = m_verticalStepSize; + } + // contentY uses reversed sign + return scrollFlickable(QPointF(0, -stepSize)); +} + +bool WheelHandler::scrollLeft(qreal stepSize) +{ + if (qFuzzyIsNull(stepSize)) { + return false; + } else if (stepSize < 0) { + stepSize = m_horizontalStepSize; + } + // contentX uses reversed sign + return scrollFlickable(QPoint(stepSize, 0)); +} + +bool WheelHandler::scrollRight(qreal stepSize) +{ + if (qFuzzyIsNull(stepSize)) { + return false; + } else if (stepSize < 0) { + stepSize = m_horizontalStepSize; + } + // contentX uses reversed sign + return scrollFlickable(QPoint(-stepSize, 0)); +} + +bool WheelHandler::eventFilter(QObject *watched, QEvent *event) +{ + auto item = qobject_cast(watched); + if (!item || !item->isEnabled()) { + return false; + } + + qreal contentWidth = 0; + qreal contentHeight = 0; + qreal pageWidth = 0; + qreal pageHeight = 0; + if (m_flickable) { + contentWidth = m_flickable->property("contentWidth").toReal(); + contentHeight = m_flickable->property("contentHeight").toReal(); + pageWidth = m_flickable->width() - m_flickable->property("leftMargin").toReal() - m_flickable->property("rightMargin").toReal(); + pageHeight = m_flickable->height() - m_flickable->property("topMargin").toReal() - m_flickable->property("bottomMargin").toReal(); + } + + // The code handling touch, mouse and hover events is mostly copied/adapted from QQuickScrollView::childMouseEventFilter() + switch (event->type()) { + case QEvent::Wheel: { + // QQuickScrollBar::interactive handling Matches behavior in QQuickScrollView::eventFilter() + if (m_filterMouseEvents) { + if (m_verticalScrollBar) { + m_verticalScrollBar->setProperty("interactive", true); + } + if (m_horizontalScrollBar) { + m_horizontalScrollBar->setProperty("interactive", true); + } + } + QWheelEvent *wheelEvent = static_cast(event); + + // Can't use wheelEvent->deviceType() to determine device type since on Wayland mouse is always regarded as touchpad + // https://invent.kde.org/qt/qt/qtwayland/-/blob/e695a39519a7629c1549275a148cfb9ab99a07a9/src/client/qwaylandinputdevice.cpp#L445 + // and we can only expect a touchpad never generates the same angle delta as a mouse + + // mouse wheel can also generate angle delta like 240, 360 and so on when scrolling very fast + // only checking wheelEvent->angleDelta().y() because we only animate for contentY + m_wasTouched = (std::abs(wheelEvent->angleDelta().y()) != 0 && std::abs(wheelEvent->angleDelta().y()) % 120 != 0); + // NOTE: On X11 with libinput, pixelDelta is identical to angleDelta when using a mouse that shouldn't use pixelDelta. + // If faulty pixelDelta, reset pixelDelta to (0,0). + if (wheelEvent->pixelDelta() == wheelEvent->angleDelta()) { + // In order to change any of the data, we have to create a whole new QWheelEvent from its constructor. + QWheelEvent newWheelEvent(wheelEvent->position(), + wheelEvent->globalPosition(), + QPoint(0, 0), // pixelDelta + wheelEvent->angleDelta(), + wheelEvent->buttons(), + wheelEvent->modifiers(), + wheelEvent->phase(), + wheelEvent->inverted(), + wheelEvent->source()); + m_kirigamiWheelEvent.initializeFromEvent(&newWheelEvent); + } else { + m_kirigamiWheelEvent.initializeFromEvent(wheelEvent); + } + + Q_EMIT wheel(&m_kirigamiWheelEvent); + + if (m_kirigamiWheelEvent.isAccepted()) { + return true; + } + + bool scrolled = false; + if (m_scrollFlickableTarget || (contentHeight <= pageHeight && contentWidth <= pageWidth)) { + // Don't use pixelDelta from the event unless angleDelta is not available + // because scrolling by pixelDelta is too slow on Wayland with libinput. + QPointF pixelDelta = m_kirigamiWheelEvent.angleDelta().isNull() ? m_kirigamiWheelEvent.pixelDelta() : QPoint(0, 0); + scrolled = scrollFlickable(pixelDelta, m_kirigamiWheelEvent.angleDelta(), Qt::KeyboardModifiers(m_kirigamiWheelEvent.modifiers())); + } + setScrolling(scrolled); + + // NOTE: Wheel events created by touchpad gestures with pixel deltas will cause scrolling to jump back + // to where scrolling started unless the event is always accepted before it reaches the Flickable. + bool flickableWillUseGestureScrolling = !(wheelEvent->source() == Qt::MouseEventNotSynthesized || wheelEvent->pixelDelta().isNull()); + return scrolled || m_blockTargetWheel || flickableWillUseGestureScrolling; + } + + case QEvent::TouchBegin: { + m_wasTouched = true; + if (!m_filterMouseEvents) { + break; + } + if (m_verticalScrollBar) { + m_verticalScrollBar->setProperty("interactive", false); + } + if (m_horizontalScrollBar) { + m_horizontalScrollBar->setProperty("interactive", false); + } + break; + } + + case QEvent::TouchEnd: { + m_wasTouched = false; + break; + } + + case QEvent::MouseButtonPress: { + // NOTE: Flickable does not handle touch events, only synthesized mouse events + m_wasTouched = static_cast(event)->source() != Qt::MouseEventNotSynthesized; + if (!m_filterMouseEvents) { + break; + } + if (!m_wasTouched) { + if (m_verticalScrollBar) { + m_verticalScrollBar->setProperty("interactive", true); + } + if (m_horizontalScrollBar) { + m_horizontalScrollBar->setProperty("interactive", true); + } + break; + } + return !m_wasTouched && item == m_flickable; + } + + case QEvent::MouseMove: + case QEvent::MouseButtonRelease: { + setScrolling(false); + if (!m_filterMouseEvents) { + break; + } + if (static_cast(event)->source() == Qt::MouseEventNotSynthesized && item == m_flickable) { + return true; + } + break; + } + + case QEvent::HoverEnter: + case QEvent::HoverMove: { + if (!m_filterMouseEvents) { + break; + } + if (m_wasTouched && (item == m_verticalScrollBar || item == m_horizontalScrollBar)) { + if (m_verticalScrollBar) { + m_verticalScrollBar->setProperty("interactive", true); + } + if (m_horizontalScrollBar) { + m_horizontalScrollBar->setProperty("interactive", true); + } + } + break; + } + + case QEvent::KeyPress: { + if (!m_keyNavigationEnabled) { + break; + } + QKeyEvent *keyEvent = static_cast(event); + bool horizontalScroll = keyEvent->modifiers() & m_defaultHorizontalScrollModifiers; + switch (keyEvent->key()) { + case Qt::Key_Up: + return scrollUp(); + case Qt::Key_Down: + return scrollDown(); + case Qt::Key_Left: + return scrollLeft(); + case Qt::Key_Right: + return scrollRight(); + case Qt::Key_PageUp: + return horizontalScroll ? scrollLeft(pageWidth) : scrollUp(pageHeight); + case Qt::Key_PageDown: + return horizontalScroll ? scrollRight(pageWidth) : scrollDown(pageHeight); + case Qt::Key_Home: + return horizontalScroll ? scrollLeft(contentWidth) : scrollUp(contentHeight); + case Qt::Key_End: + return horizontalScroll ? scrollRight(contentWidth) : scrollDown(contentHeight); + default: + break; + } + break; + } + + default: + break; + } + + return false; +} + +#include "moc_wheelhandler.cpp" diff --git a/src/wheelhandler.h b/src/wheelhandler.h new file mode 100644 index 0000000..70ff9d9 --- /dev/null +++ b/src/wheelhandler.h @@ -0,0 +1,401 @@ +/* SPDX-FileCopyrightText: 2019 Marco Martin + * SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "platform/settings.h" +#include "platform/units.h" + +class QWheelEvent; +class QQmlEngine; +class WheelHandler; + +/** + * Describes the mouse wheel event + */ +class KirigamiWheelEvent : public QObject +{ + Q_OBJECT + QML_NAMED_ELEMENT(WheelEvent) + QML_UNCREATABLE("") + + /** + * x: real + * + * X coordinate of the mouse pointer + */ + Q_PROPERTY(qreal x READ x CONSTANT FINAL) + + /** + * y: real + * + * Y coordinate of the mouse pointer + */ + Q_PROPERTY(qreal y READ y CONSTANT FINAL) + + /** + * angleDelta: point + * + * The distance the wheel is rotated in degrees. + * The x and y coordinates indicate the horizontal and vertical wheels respectively. + * A positive value indicates it was rotated up/right, negative, bottom/left + * This value is more likely to be set in traditional mice. + */ + Q_PROPERTY(QPointF angleDelta READ angleDelta CONSTANT FINAL) + + /** + * pixelDelta: point + * + * provides the delta in screen pixels available on high resolution trackpads + */ + Q_PROPERTY(QPointF pixelDelta READ pixelDelta CONSTANT FINAL) + + /** + * buttons: int + * + * it contains an OR combination of the buttons that were pressed during the wheel, they can be: + * Qt.LeftButton, Qt.MiddleButton, Qt.RightButton + */ + Q_PROPERTY(int buttons READ buttons CONSTANT FINAL) + + /** + * modifiers: int + * + * Keyboard mobifiers that were pressed during the wheel event, such as: + * Qt.NoModifier (default, no modifiers) + * Qt.ControlModifier + * Qt.ShiftModifier + * ... + */ + Q_PROPERTY(int modifiers READ modifiers CONSTANT FINAL) + + /** + * inverted: bool + * + * Whether the delta values are inverted + * On some platformsthe returned delta are inverted, so positive values would mean bottom/left + */ + Q_PROPERTY(bool inverted READ inverted CONSTANT FINAL) + + /** + * accepted: bool + * + * If set, the event shouldn't be managed anymore, + * for instance it can be used to block the handler to manage the scroll of a view on some scenarios + * @code + * // This handler handles automatically the scroll of + * // flickableItem, unless Ctrl is pressed, in this case the + * // app has custom code to handle Ctrl+wheel zooming + * Kirigami.WheelHandler { + * target: flickableItem + * blockTargetWheel: true + * scrollFlickableTarget: true + * onWheel: { + * if (wheel.modifiers & Qt.ControlModifier) { + * wheel.accepted = true; + * // Handle scaling of the view + * } + * } + * } + * @endcode + * + */ + Q_PROPERTY(bool accepted READ isAccepted WRITE setAccepted FINAL) + +public: + KirigamiWheelEvent(QObject *parent = nullptr); + ~KirigamiWheelEvent() override; + + void initializeFromEvent(QWheelEvent *event); + + qreal x() const; + qreal y() const; + QPointF angleDelta() const; + QPointF pixelDelta() const; + int buttons() const; + int modifiers() const; + bool inverted() const; + bool isAccepted(); + void setAccepted(bool accepted); + +private: + qreal m_x = 0; + qreal m_y = 0; + QPointF m_angleDelta; + QPointF m_pixelDelta; + Qt::MouseButtons m_buttons = Qt::NoButton; + Qt::KeyboardModifiers m_modifiers = Qt::NoModifier; + bool m_inverted = false; + bool m_accepted = false; +}; + +class WheelFilterItem : public QQuickItem +{ + Q_OBJECT +public: + WheelFilterItem(QQuickItem *parent = nullptr); +}; + +/** + * @brief Handles scrolling for a Flickable and 2 attached ScrollBars. + * + * WheelHandler filters events from a Flickable, a vertical ScrollBar and a horizontal ScrollBar. + * Wheel and KeyPress events (when `keyNavigationEnabled` is true) are used to scroll the Flickable. + * When `filterMouseEvents` is true, WheelHandler blocks mouse button input from reaching the Flickable + * and sets the `interactive` property of the scrollbars to false when touch input is used. + * + * Wheel event handling behavior: + * + * - Pixel delta is ignored unless angle delta is not available because pixel delta scrolling is too slow. Qt Widgets doesn't use pixel delta either, so the + * default scroll speed should be consistent with Qt Widgets. + * - When using angle delta, scroll using the step increments defined by `verticalStepSize` and `horizontalStepSize`. + * - When one of the keyboard modifiers in `pageScrollModifiers` is used, scroll by pages. + * - When using a device that doesn't use 120 angle delta unit increments such as a touchpad, the `verticalStepSize`, `horizontalStepSize` and page increments + * (if using page scrolling) will be multiplied by `angle delta / 120` to keep scrolling smooth. + * - If scrolling has happened in the last 400ms, use an internal QQuickItem stacked over the Flickable's contentItem to catch wheel events and use those wheel + * events to scroll, if possible. This prevents controls inside the Flickable's contentItem that allow scrolling to change the value (e.g., Sliders, SpinBoxes) + * from conflicting with scrolling the page. + * + * Common usage with a Flickable: + * + * @include wheelhandler/FlickableUsage.qml + * + * Common usage inside of a ScrollView template: + * + * @include wheelhandler/ScrollViewUsage.qml + * + */ +class WheelHandler : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + QML_ELEMENT + + /** + * @brief This property holds the Qt Quick Flickable that the WheelHandler will control. + */ + Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged FINAL) + + /** + * @brief This property holds the vertical step size. + * + * The default value is equivalent to `20 * Qt.styleHints.wheelScrollLines`. This is consistent with the default increment for QScrollArea. + * + * @sa horizontalStepSize + * + * @since KDE Frameworks 5.89 + */ + Q_PROPERTY(qreal verticalStepSize READ verticalStepSize WRITE setVerticalStepSize RESET resetVerticalStepSize NOTIFY verticalStepSizeChanged FINAL) + + /** + * @brief This property holds the horizontal step size. + * + * The default value is equivalent to `20 * Qt.styleHints.wheelScrollLines`. This is consistent with the default increment for QScrollArea. + * + * @sa verticalStepSize + * + * @since KDE Frameworks 5.89 + */ + Q_PROPERTY( + qreal horizontalStepSize READ horizontalStepSize WRITE setHorizontalStepSize RESET resetHorizontalStepSize NOTIFY horizontalStepSizeChanged FINAL) + + /** + * @brief This property holds the keyboard modifiers that will be used to start page scrolling. + * + * The default value is equivalent to `Qt.ControlModifier | Qt.ShiftModifier`. This matches QScrollBar, which uses QAbstractSlider behavior. + * + * @since KDE Frameworks 5.89 + */ + Q_PROPERTY(Qt::KeyboardModifiers pageScrollModifiers READ pageScrollModifiers WRITE setPageScrollModifiers RESET resetPageScrollModifiers NOTIFY + pageScrollModifiersChanged FINAL) + + /** + * @brief This property holds whether the WheelHandler filters mouse events like a Qt Quick Controls ScrollView would. + * + * Touch events are allowed to flick the view and they make the scrollbars not interactive. + * + * Mouse events are not allowed to flick the view and they make the scrollbars interactive. + * + * Hover events on the scrollbars and wheel events on anything also make the scrollbars interactive when this property is set to true. + * + * The default value is `false`. + * + * @since KDE Frameworks 5.89 + */ + Q_PROPERTY(bool filterMouseEvents READ filterMouseEvents WRITE setFilterMouseEvents NOTIFY filterMouseEventsChanged FINAL) + + /** + * @brief This property holds whether the WheelHandler handles keyboard scrolling. + * + * - Left arrow scrolls a step to the left. + * - Right arrow scrolls a step to the right. + * - Up arrow scrolls a step upwards. + * - Down arrow scrolls a step downwards. + * - PageUp scrolls to the previous page. + * - PageDown scrolls to the next page. + * - Home scrolls to the beginning. + * - End scrolls to the end. + * - When Alt is held, scroll horizontally when using PageUp, PageDown, Home or End. + * + * The default value is `false`. + * + * @since KDE Frameworks 5.89 + */ + Q_PROPERTY(bool keyNavigationEnabled READ keyNavigationEnabled WRITE setKeyNavigationEnabled NOTIFY keyNavigationEnabledChanged FINAL) + + /** + * @brief This property holds whether the WheelHandler blocks all wheel events from reaching the Flickable. + * + * When this property is false, scrolling the Flickable with WheelHandler will only block an event from reaching the Flickable if the Flickable is actually + * scrolled by WheelHandler. + * + * NOTE: Wheel events created by touchpad gestures with pixel deltas will always be accepted no matter what. This is because they will cause the Flickable + * to jump back to where scrolling started unless the events are always accepted before they reach the Flickable. + * + * The default value is true. + */ + Q_PROPERTY(bool blockTargetWheel MEMBER m_blockTargetWheel NOTIFY blockTargetWheelChanged FINAL) + + /** + * @brief This property holds whether the WheelHandler can use wheel events to scroll the Flickable. + * + * The default value is true. + */ + Q_PROPERTY(bool scrollFlickableTarget MEMBER m_scrollFlickableTarget NOTIFY scrollFlickableTargetChanged FINAL) + +public: + explicit WheelHandler(QObject *parent = nullptr); + ~WheelHandler() override; + + QQuickItem *target() const; + void setTarget(QQuickItem *target); + + qreal verticalStepSize() const; + void setVerticalStepSize(qreal stepSize); + void resetVerticalStepSize(); + + qreal horizontalStepSize() const; + void setHorizontalStepSize(qreal stepSize); + void resetHorizontalStepSize(); + + Qt::KeyboardModifiers pageScrollModifiers() const; + void setPageScrollModifiers(Qt::KeyboardModifiers modifiers); + void resetPageScrollModifiers(); + + bool filterMouseEvents() const; + void setFilterMouseEvents(bool enabled); + + bool keyNavigationEnabled() const; + void setKeyNavigationEnabled(bool enabled); + + /** + * Scroll up one step. If the stepSize parameter is less than 0, the verticalStepSize will be used. + * + * returns true if the contentItem was moved. + * + * @since KDE Frameworks 5.89 + */ + Q_INVOKABLE bool scrollUp(qreal stepSize = -1); + + /** + * Scroll down one step. If the stepSize parameter is less than 0, the verticalStepSize will be used. + * + * returns true if the contentItem was moved. + * + * @since KDE Frameworks 5.89 + */ + Q_INVOKABLE bool scrollDown(qreal stepSize = -1); + + /** + * Scroll left one step. If the stepSize parameter is less than 0, the horizontalStepSize will be used. + * + * returns true if the contentItem was moved. + * + * @since KDE Frameworks 5.89 + */ + Q_INVOKABLE bool scrollLeft(qreal stepSize = -1); + + /** + * Scroll right one step. If the stepSize parameter is less than 0, the horizontalStepSize will be used. + * + * returns true if the contentItem was moved. + * + * @since KDE Frameworks 5.89 + */ + Q_INVOKABLE bool scrollRight(qreal stepSize = -1); + +Q_SIGNALS: + void targetChanged(); + void verticalStepSizeChanged(); + void horizontalStepSizeChanged(); + void pageScrollModifiersChanged(); + void filterMouseEventsChanged(); + void keyNavigationEnabledChanged(); + void blockTargetWheelChanged(); + void scrollFlickableTargetChanged(); + + /** + * @brief This signal is emitted when a wheel event reaches the event filter, just before scrolling is handled. + * + * Accepting the wheel event in the `onWheel` signal handler prevents scrolling from happening. + */ + void wheel(KirigamiWheelEvent *wheel); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + +private Q_SLOTS: + void _k_rebindScrollBars(); + +private: + void classBegin() override; + void componentComplete() override; + void initSmoothScrollDuration(); + + void setScrolling(bool scrolling); + bool scrollFlickable(QPointF pixelDelta, QPointF angleDelta = {}, Qt::KeyboardModifiers modifiers = Qt::NoModifier); + + Kirigami::Platform::Units *m_units = nullptr; + Kirigami::Platform::Settings *m_settings = nullptr; + QPointer m_flickable; + QPointer m_verticalScrollBar; + QPointer m_horizontalScrollBar; + QMetaObject::Connection m_verticalChangedConnection; + QMetaObject::Connection m_horizontalChangedConnection; + QPointer m_filterItem; + // Matches QScrollArea and QTextEdit + qreal m_defaultPixelStepSize = 20 * QGuiApplication::styleHints()->wheelScrollLines(); + qreal m_verticalStepSize = m_defaultPixelStepSize; + qreal m_horizontalStepSize = m_defaultPixelStepSize; + bool m_explicitVStepSize = false; + bool m_explicitHStepSize = false; + bool m_wheelScrolling = false; + constexpr static qreal m_wheelScrollingDuration = 400; + bool m_filterMouseEvents = false; + bool m_keyNavigationEnabled = false; + bool m_blockTargetWheel = true; + bool m_scrollFlickableTarget = true; + // Same as QXcbWindow. + constexpr static Qt::KeyboardModifiers m_defaultHorizontalScrollModifiers = Qt::AltModifier; + // Same as QScrollBar/QAbstractSlider. + constexpr static Qt::KeyboardModifiers m_defaultPageScrollModifiers = Qt::ControlModifier | Qt::ShiftModifier; + Qt::KeyboardModifiers m_pageScrollModifiers = m_defaultPageScrollModifiers; + QTimer m_wheelScrollingTimer; + KirigamiWheelEvent m_kirigamiWheelEvent; + + // Smooth scrolling + QQmlEngine *m_engine = nullptr; + QPropertyAnimation m_yScrollAnimation{nullptr, "contentY"}; + bool m_wasTouched = false; +}; diff --git a/templates/.clang-format b/templates/.clang-format new file mode 100644 index 0000000..e86e818 --- /dev/null +++ b/templates/.clang-format @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: None +# SPDX-License-Identifier: CC0-1.0 +DisableFormat: true +SortIncludes: false diff --git a/templates/CMakeLists.txt b/templates/CMakeLists.txt new file mode 100644 index 0000000..db98ac0 --- /dev/null +++ b/templates/CMakeLists.txt @@ -0,0 +1 @@ +kde_package_app_templates(TEMPLATES kirigami6 INSTALL_DIR ${KDE_INSTALL_KAPPTEMPLATESDIR}) diff --git a/templates/kirigami6/CMakeLists.txt b/templates/kirigami6/CMakeLists.txt new file mode 100644 index 0000000..3f5b0a5 --- /dev/null +++ b/templates/kirigami6/CMakeLists.txt @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) + +project(%{APPNAMELC} VERSION 0.1) + +include(FeatureSummary) + +set(QT6_MIN_VERSION 6.5.0) +set(KF6_MIN_VERSION 6.0.0) + +find_package(ECM ${KF6_MIN_VERSION} REQUIRED NO_MODULE) + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH}) + +include(FeatureSummary) +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDECompilerSettings NO_POLICY_SCOPE) +include(ECMSetupVersion) +include(ECMFindQmlModule) +if (NOT ANDROID) + include(KDEGitCommitHooks) + include(KDEClangFormat) +endif() + +ecm_setup_version(${PROJECT_VERSION} + VARIABLE_PREFIX %{APPNAMEUC} + VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/src/version-%{APPNAMELC}.h" +) + +set(QT_EXTRA_COMPONENTS) +if (NOT ANDROID) + list(APPEND QT_EXTRA_COMPONENTS Widgets) +endif() + +find_package(Qt6 ${QT6_MIN_VERSION} REQUIRED COMPONENTS Core Gui Qml QuickControls2 Svg ${QT_EXTRA_COMPONENTS}) +find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS Kirigami CoreAddons Config I18n) + +qt_policy(SET QTP0001 NEW) + +ecm_find_qmlmodule(org.kde.kirigamiaddons.formcard 1.0) + +if (ANDROID) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle) +endif() + +add_subdirectory(src) + +install(FILES org.kde.%{APPNAMELC}.desktop DESTINATION ${KDE_INSTALL_APPDIR}) +install(FILES org.kde.%{APPNAMELC}.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) +ki18n_install(po) + +feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) + +if (NOT ANDROID) + file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES src/*.cpp src/*.h) + kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES}) + kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) +endif() diff --git a/templates/kirigami6/LICENSES/BSD-3-Clause.txt b/templates/kirigami6/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000..0741db7 --- /dev/null +++ b/templates/kirigami6/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,26 @@ +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/templates/kirigami6/LICENSES/CC0-1.0.txt b/templates/kirigami6/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/templates/kirigami6/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/templates/kirigami6/LICENSES/FSFAP.txt b/templates/kirigami6/LICENSES/FSFAP.txt new file mode 100644 index 0000000..c96c65e --- /dev/null +++ b/templates/kirigami6/LICENSES/FSFAP.txt @@ -0,0 +1,3 @@ +Copying and distribution of this file, with or without modification, are permitted +in any medium without royalty provided the copyright notice and this notice +are preserved. This file is offered as-is, without any warranty. diff --git a/templates/kirigami6/LICENSES/GPL-2.0-or-later.txt b/templates/kirigami6/LICENSES/GPL-2.0-or-later.txt new file mode 100644 index 0000000..3b6070f --- /dev/null +++ b/templates/kirigami6/LICENSES/GPL-2.0-or-later.txt @@ -0,0 +1,311 @@ +GNU GENERAL PUBLIC LICENSE +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software +is covered by the GNU Lesser General Public License instead.) You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must give the recipients all the rights that you have. You +must make sure that they, too, receive or can get the source code. And you +must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If +the software is modified by someone else and passed on, we want its recipients +to know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will individually +obtain patent licenses, in effect making the program proprietary. To prevent +this, we have made it clear that any patent must be licensed for everyone's +free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms +of this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or translated +into another language. (Hereinafter, translation is included without limitation +in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running the Program +is not restricted, and the output from the Program is covered only if its +contents constitute a work based on the Program (independent of having been +made by running the Program). Whether that is true depends on what the Program +does. + +1. You may copy and distribute verbatim copies of the Program's source code +as you receive it, in any medium, provided that you conspicuously and appropriately +publish on each copy an appropriate copyright notice and disclaimer of warranty; +keep intact all the notices that refer to this License and to the absence +of any warranty; and give any other recipients of the Program a copy of this +License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, +thus forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + +a) You must cause the modified files to carry prominent notices stating that +you changed the files and the date of any change. + +b) You must cause any work that you distribute or publish, that in whole or +in part contains or is derived from the Program or any part thereof, to be +licensed as a whole at no charge to all third parties under the terms of this +License. + +c) If the modified program normally reads commands interactively when run, +you must cause it, when started running for such interactive use in the most +ordinary way, to print or display an announcement including an appropriate +copyright notice and a notice that there is no warranty (or else, saying that +you provide a warranty) and that users may redistribute the program under +these conditions, and telling the user how to view a copy of this License. +(Exception: if the Program itself is interactive but does not normally print +such an announcement, your work based on the Program is not required to print +an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Program, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Program. + +In addition, mere aggregation of another work not based on the Program with +the Program (or with a work based on the Program) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under Section +2) in object code or executable form under the terms of Sections 1 and 2 above +provided that you also do one of the following: + +a) Accompany it with the complete corresponding machine-readable source code, +which must be distributed under the terms of Sections 1 and 2 above on a medium +customarily used for software interchange; or, + +b) Accompany it with a written offer, valid for at least three years, to give +any third party, for a charge no more than your cost of physically performing +source distribution, a complete machine-readable copy of the corresponding +source code, to be distributed under the terms of Sections 1 and 2 above on +a medium customarily used for software interchange; or, + +c) Accompany it with the information you received as to the offer to distribute +corresponding source code. (This alternative is allowed only for noncommercial +distribution and only if you received the program in object code or executable +form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +If distribution of executable or object code is made by offering access to +copy from a designated place, then offering equivalent access to copy the +source code from the same place counts as distribution of the source code, +even though third parties are not compelled to copy the source along with +the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except +as expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses terminated +so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Program or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Program +(or any work based on the Program), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor +to copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of +the rights granted herein. You are not responsible for enforcing compliance +by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Program at all. For example, if a +patent license would not permit royalty-free redistribution of the Program +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system, which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Program under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of +the General Public License from time to time. Such new versions will be similar +in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Program does not specify a version number of this License, you may choose +any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing and reuse +of software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + +one line to give the program's name and an idea of what it does. Copyright +(C) yyyy name of author + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 51 Franklin +Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how +to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when +it starts in an interactive mode: + +Gnomovision version 69, Copyright (C) year name of author Gnomovision comes +with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, +and you are welcome to redistribute it under certain conditions; type `show +c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than `show w' and `show c'; they could even be mouse-clicks +or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' +(which makes passes at compilers) written by James Hacker. + +signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice diff --git a/templates/kirigami6/android/AndroidManifest.xml b/templates/kirigami6/android/AndroidManifest.xml new file mode 100644 index 0000000..ca663ec --- /dev/null +++ b/templates/kirigami6/android/AndroidManifest.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/kirigami6/android/build.gradle b/templates/kirigami6/android/build.gradle new file mode 100644 index 0000000..fa62a26 --- /dev/null +++ b/templates/kirigami6/android/build.gradle @@ -0,0 +1,83 @@ +/* + SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + SPDX-License-Identifier: BSD-3-Clause +*/ + +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.4.1' + } +} + +repositories { + google() + jcenter() +} + + +apply plugin: 'com.android.application' +apply from: '../version.gradle' +def timestamp = (int)(new Date().getTime()/1000) + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) +} + +android { + /******************************************************* + * The following variables: + * - androidBuildToolsVersion, + * - androidCompileSdkVersion + * - qtAndroidDir - holds the path to qt android files + * needed to build any Qt application + * on Android. + * + * are defined in gradle.properties file. This file is + * updated by QtCreator and androiddeployqt tools. + * Changing them manually might break the compilation! + *******************************************************/ + + compileSdkVersion androidCompileSdkVersion + buildToolsVersion androidBuildToolsVersion + ndkVersion androidNdkVersion + packagingOptions.jniLibs.useLegacyPackaging true + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = [qtAndroidDir + '/src', 'src', 'java'] + aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl'] + res.srcDirs = [qtAndroidDir + '/res', 'res'] + resources.srcDirs = ['src'] + renderscript.srcDirs = ['src'] + assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['libs'] + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + lintOptions { + abortOnError false + } + + defaultConfig { + minSdkVersion qtMinSdkVersion + targetSdkVersion qtTargetSdkVersion + applicationId "org.kde.%{APPNAMELC}" + namespace "org.kde.%{APPNAMELC}" + versionCode timestamp + versionName projectVersionFull + manifestPlaceholders = [versionName: projectVersionFull, versionCode: timestamp] + } + +} + diff --git a/templates/kirigami6/android/res/drawable/logo.png b/templates/kirigami6/android/res/drawable/logo.png new file mode 100644 index 0000000..a63448d Binary files /dev/null and b/templates/kirigami6/android/res/drawable/logo.png differ diff --git a/templates/kirigami6/android/res/drawable/splash.xml b/templates/kirigami6/android/res/drawable/splash.xml new file mode 100644 index 0000000..d69509e --- /dev/null +++ b/templates/kirigami6/android/res/drawable/splash.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/templates/kirigami6/android/version.gradle.in b/templates/kirigami6/android/version.gradle.in new file mode 100644 index 0000000..9ddd1bc --- /dev/null +++ b/templates/kirigami6/android/version.gradle.in @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> +// SPDX-License-Identifier: BSD-3-Clause + +ext { + projectVersionFull = "@PROJECT_VERSION@" +} + diff --git a/templates/kirigami6/kirigami-app.png b/templates/kirigami6/kirigami-app.png new file mode 100644 index 0000000..3d72ce7 Binary files /dev/null and b/templates/kirigami6/kirigami-app.png differ diff --git a/templates/kirigami6/kirigami.kdevtemplate b/templates/kirigami6/kirigami.kdevtemplate new file mode 100644 index 0000000..e96af76 --- /dev/null +++ b/templates/kirigami6/kirigami.kdevtemplate @@ -0,0 +1,82 @@ +# KDE Config File +[General] +Name=Kirigami Application (Qt6) +Name[ar]=تطبيق كيريغامي( كيوت 6) +Name[bg]=Приложение на Kirigami (Qt6) +Name[ca]=Aplicació de Kirigami (Qt6) +Name[ca@valencia]=Aplicació de Kirigami (Qt6) +Name[cs]=Aplikace Kirigami (Qt6) +Name[de]=Kirigami-Anwendung (Qt6) +Name[en_GB]=Kirigami Application (Qt6) +Name[eo]=Kirigami-Aplikaĵo (Qt6) +Name[es]=Aplicación de Kirigami (Qt6) +Name[eu]=Kirigami aplikazioa (Qt6) +Name[fi]=Kirigami-sovellus (Qt6) +Name[fr]=Application sous Kirigami (Qt6) +Name[gl]=Aplicación de Kirigami (Qt6) +Name[he]=יישום Kirigami ‏(Qt6) +Name[hu]=Kirigami alkalmazás (Qt6) +Name[ia]=Application de Kirigami (Qt6) +Name[is]=Kirigami-forrit (Qt6) +Name[it]=Applicazione Kirigami (Qt6) +Name[ka]=აპლიკაცია Kirigami (Qt6) +Name[ko]=Kirigami 앱(Qt6) +Name[lt]=Kirigami programa (Qt6) +Name[lv]=„Kirigami“ programma (Qt6) +Name[nb]=Kirigami-program (Qt6) +Name[nl]=Kirigami-toepassing (Qt6) +Name[nn]=Kirigami-program (Qt6) +Name[pl]=Aplikacja Kirigami (Qt6) +Name[pt_BR]=Aplicativo Kirigami (Qt6) +Name[ru]=Приложение Kirigami (Qt6) +Name[sa]=किरिगामी अनुप्रयोग (Qt6) 1.1. +Name[sl]=Aplikacija Kirigami (Qt6) +Name[sv]=Kirigami-program (Qt6) +Name[tr]=Kirigami Uygulaması (Qt6) +Name[uk]=Програма на Kirigami (Qt6) +Name[x-test]=xxKirigami Application (Qt6)xx +Name[zh_CN]=Kirigami 应用程序 (Qt6) +Name[zh_TW]=Kirigami 應用程式 (Qt6) +Comment=Convergent application using Qt Quick Controls 2 and Kirigami +Comment[ar]=تطبيق متقارب باستخدام QT Quick Controls 2 و كيريغامي +Comment[az]=Qt Quick Controls 2 və Kirigami istifadə edən istifadəçi dostu proqrams +Comment[bg]=Конвергиране на приложение чрез Qt Quick Controls 2 и Kirigami +Comment[ca]=Aplicació convergent que usa els Qt Quick Controls 2 i el Kirigami +Comment[ca@valencia]=Aplicació convergent que utilitza Qt Quick Controls 2 i Kirigami +Comment[de]=Eine konvergente Anwendung mit Qt Quick Controls 2 und Kirigami +Comment[en_GB]=Convergent application using Qt Quick Controls 2 and Kirigami +Comment[eo]=Konverĝa aplikaĵo uzanta Qt Quick Controls 2 kaj Kirigami +Comment[es]=Aplicación convergente que usa Qt Quick Controls 2 y Kirigami +Comment[eu]=«Qt Quick Controls 2» eta Kirigami erabiltzen dituen aplikazio konbergente bat +Comment[fi]=Qt Quick Controls 2:ta ja Kirigamia käyttävä mukautuva sovellus +Comment[fr]=Une application convergente utilisant les modules « Qt QuickControls2 » et Kirigami +Comment[gl]=Aplicación converxente que usa Qt Quick Controls 2 e Kirigami. +Comment[he]=יישום אחוד שמשתמש בפקדים מהירים 2 של Qt וב־Kirigami +Comment[hu]=Qt Quick Controls 2-t és Kirigamit használó konvergens alkalmazás +Comment[ia]=Application convergente usante Controlos Rapide de QT e Kirigami +Comment[is]=Samþætt forrit sem notar Qt Quick Controls 2 og Kirigami +Comment[it]=Applicazione convergente che utilizza Qt Quick Controls 2 e Kirigami +Comment[ka]=კონვერგენტული პროგრამა Qt Quick Controls 2-ის და Kirigami-ის გამოყენებით +Comment[ko]=Qt Quick Controls 2와 Kirigami를 사용하는 통합형 앱 +Comment[lt]=Konverguojanti programa naudojanti Qt Quick valdiklius 2 ir Kirigami +Comment[lv]=KonverÄ£enta programma, izmantojot „Qt Quick Controls“ 2 un “Kirigami“ +Comment[nb]=Program for ulike typer brukergrensesnitt med Qt Quick Controls 2 og Kirigami +Comment[nl]=Convergente toepassing met Qt Quick Controls 2 en Kirigami +Comment[nn]=Program for ulike typar brukarflater med Qt Quick Controls 2 og Kirigami +Comment[pl]=Adaptująca się aplikacja, używająca Qt Quick Controls 2 oraz Kirigami +Comment[pt]=Aplicação convergente que usa os Qt Quick Controls 2 e o Kirigami +Comment[pt_BR]=Aplicativo convergente usando Qt Quick Controls 2 e Kirigami +Comment[ro]=Aplicație convergentă folosind Qt Quick Controls 2 și Kirigami +Comment[ru]=Конвергентное приложение, использующее Qt Quick Controls 2 и Kirigami +Comment[sa]=Qt Quick Controls 2 तथा Kirigami इत्यस्य उपयोगेन अभिसरणीयः अनुप्रयोगः +Comment[sl]=Konvergentna aplikacija, ki uporablja kontrole Qt Quick in Kirigami +Comment[sv]=Konvergent program som använder Qt Quick Controls 2 och Kirigami +Comment[tr]=Qt Quick Controls 2 ve Kirigami kullanan yakınsak uygulama +Comment[uk]=Зручна програма з використанням засобів керування Qt Quick 2 та Kirigami +Comment[x-test]=xxConvergent application using Qt Quick Controls 2 and Kirigamixx +Comment[zh_CN]=基于 Qt Quick Controls 2 和 Kirigami 构建的桌面与移动平台通用应用程序 +Comment[zh_TW]=使用 Qt Quick Controls 2 和 Kirigami 的跨平台應用程式 + +Category=Qt/Graphical +Icon=kirigami-app.png +ShowFilesAfterGeneration=src/main.cpp diff --git a/templates/kirigami6/org.kde.%{APPNAMELC}.desktop b/templates/kirigami6/org.kde.%{APPNAMELC}.desktop new file mode 100644 index 0000000..dfe88e9 --- /dev/null +++ b/templates/kirigami6/org.kde.%{APPNAMELC}.desktop @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> +[Desktop Entry] +Name=%{APPNAME} +Comment=%{APPNAME} Kirigami Application +Version=1.0 +Exec=%{APPNAMELC} +Icon=applications-development +Type=Application +Terminal=false +# Add an actual main category here (and possibly applicable additional ones) +# https://specifications.freedesktop.org/menu-spec/latest/apa.html#main-category-registry +Categories=Qt;KDE; diff --git a/templates/kirigami6/org.kde.%{APPNAMELC}.json b/templates/kirigami6/org.kde.%{APPNAMELC}.json new file mode 100644 index 0000000..30e90f5 --- /dev/null +++ b/templates/kirigami6/org.kde.%{APPNAMELC}.json @@ -0,0 +1,27 @@ +{ + "id": "org.kde.%{APPNAMELC}", + "runtime": "org.kde.Platform", + "runtime-version": "6.8", + "sdk": "org.kde.Sdk", + "command": "%{APPNAMELC}", + "tags": ["nightly"], + "desktop-file-name-suffix": " (Nightly)", + "finish-args": [ + "--share=ipc", + "--share=network", + "--socket=x11", + "--socket=wayland", + "--device=dri" + ], + "separate-locales": false, + + "modules": [ + { + "name": "%{APPNAMELC}", + "buildsystem": "cmake-ninja", + "builddir": true, + "sources": [ { "type": "dir", "path": ".", "skip": [".git"] } ] + } + ] +} + diff --git a/templates/kirigami6/org.kde.%{APPNAMELC}.metainfo.xml b/templates/kirigami6/org.kde.%{APPNAMELC}.metainfo.xml new file mode 100644 index 0000000..795ef61 --- /dev/null +++ b/templates/kirigami6/org.kde.%{APPNAMELC}.metainfo.xml @@ -0,0 +1,22 @@ + + + + org.kde.%{APPNAMELC} + %{APPNAME} Kirigami Application + A short summary describing what this software is about + A permissive license for this metadata, e.g. "FSFAP" + Update the SPDX tags above! + The license of this software as SPDX string, e.g. "GPL-2.0-or-later" + The software vendor name, e.g. "ACME Corporation" + +

Multiple paragraphs of long description, describing this software component.

+

You can also use ordered and unordered lists:

+
    +
  • Feature 1
  • +
  • Feature 2
  • +
+

Keep in mind to XML-escape characters, and that this is not HTML markup.

+
+
diff --git a/templates/kirigami6/src/%{APPNAMELC}config.kcfg b/templates/kirigami6/src/%{APPNAMELC}config.kcfg new file mode 100644 index 0000000..1c0dac1 --- /dev/null +++ b/templates/kirigami6/src/%{APPNAMELC}config.kcfg @@ -0,0 +1,16 @@ + + + + + + + true + + + diff --git a/templates/kirigami6/src/%{APPNAMELC}config.kcfgc b/templates/kirigami6/src/%{APPNAMELC}config.kcfgc new file mode 100644 index 0000000..f112ed3 --- /dev/null +++ b/templates/kirigami6/src/%{APPNAMELC}config.kcfgc @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> +# SPDX-License-Identifier: LGPL-2.0-or-later + +File=%{APPNAMELC}config.kcfg +ClassName=%{APPNAME}Config +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true +Singleton=true diff --git a/templates/kirigami6/src/CMakeLists.txt b/templates/kirigami6/src/CMakeLists.txt new file mode 100644 index 0000000..9811337 --- /dev/null +++ b/templates/kirigami6/src/CMakeLists.txt @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: BSD-2-Clause +# SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + +# Target: static library +qt_add_qml_module(%{APPNAMELC}_static + STATIC + URI org.kde.%{APPNAMELC} + VERSION 1.0 + QML_FILES + contents/ui/Main.qml + contents/ui/About.qml +) + +target_sources(%{APPNAMELC}_static PUBLIC + app.cpp +) + +target_link_libraries(%{APPNAMELC}_static PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Qml + Qt6::Quick + Qt6::QuickControls2 + Qt6::Svg + KF6::I18n + KF6::CoreAddons + KF6::ConfigCore + KF6::ConfigGui +) +target_include_directories(%{APPNAMELC}_static PUBLIC ${CMAKE_BINARY_DIR}) + +if (ANDROID) + kirigami_package_breeze_icons(ICONS + list-add + help-about + application-exit + applications-graphics + ) +else() + target_link_libraries(%{APPNAMELC}_static PUBLIC Qt::Widgets) +endif() + +kconfig_add_kcfg_files(%{APPNAMELC}_static GENERATE_MOC %{APPNAMELC}config.kcfgc) + +# Target: main executable +add_executable(%{APPNAMELC} main.cpp) +target_link_libraries(%{APPNAMELC} PUBLIC %{APPNAMELC}_static %{APPNAMELC}_staticplugin) +install(TARGETS %{APPNAMELC} ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/templates/kirigami6/src/app.cpp b/templates/kirigami6/src/app.cpp new file mode 100644 index 0000000..c511ee9 --- /dev/null +++ b/templates/kirigami6/src/app.cpp @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + +#include "app.h" +#include +#include +#include + +void App::restoreWindowGeometry(QQuickWindow *window, const QString &group) const +{ + KConfig dataResource(QStringLiteral("data"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation); + KConfigGroup windowGroup(&dataResource, QStringLiteral("Window-") + group); + KWindowConfig::restoreWindowSize(window, windowGroup); + KWindowConfig::restoreWindowPosition(window, windowGroup); +} + +void App::saveWindowGeometry(QQuickWindow *window, const QString &group) const +{ + KConfig dataResource(QStringLiteral("data"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation); + KConfigGroup windowGroup(&dataResource, QStringLiteral("Window-") + group); + KWindowConfig::saveWindowPosition(window, windowGroup); + KWindowConfig::saveWindowSize(window, windowGroup); + dataResource.sync(); +} + +#include "moc_app.cpp" diff --git a/templates/kirigami6/src/app.h b/templates/kirigami6/src/app.h new file mode 100644 index 0000000..d487a0c --- /dev/null +++ b/templates/kirigami6/src/app.h @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + +#pragma once + +#include +#include + +class QQuickWindow; + +class App : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + // Restore current window geometry + Q_INVOKABLE void restoreWindowGeometry(QQuickWindow *window, const QString &group = QStringLiteral("main")) const; + // Save current window geometry + Q_INVOKABLE void saveWindowGeometry(QQuickWindow *window, const QString &group = QStringLiteral("main")) const; +}; diff --git a/templates/kirigami6/src/contents/ui/About.qml b/templates/kirigami6/src/contents/ui/About.qml new file mode 100644 index 0000000..92a9f0a --- /dev/null +++ b/templates/kirigami6/src/contents/ui/About.qml @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + +import org.kde.kirigamiaddons.formcard as FormCard +import org.kde.coreaddons + +FormCard.AboutPage { + aboutData: AboutData +} diff --git a/templates/kirigami6/src/contents/ui/Main.qml b/templates/kirigami6/src/contents/ui/Main.qml new file mode 100644 index 0000000..186668b --- /dev/null +++ b/templates/kirigami6/src/contents/ui/Main.qml @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami +import org.kde.%{APPNAMELC} + +Kirigami.ApplicationWindow { + id: root + + title: i18n("%{APPNAME}") + + minimumWidth: Kirigami.Units.gridUnit * 20 + minimumHeight: Kirigami.Units.gridUnit * 20 + + onClosing: App.saveWindowGeometry(root) + + onWidthChanged: saveWindowGeometryTimer.restart() + onHeightChanged: saveWindowGeometryTimer.restart() + onXChanged: saveWindowGeometryTimer.restart() + onYChanged: saveWindowGeometryTimer.restart() + + Component.onCompleted: App.restoreWindowGeometry(root) + + // This timer allows to batch update the window size change to reduce + // the io load and also work around the fact that x/y/width/height are + // changed when loading the page and overwrite the saved geometry from + // the previous session. + Timer { + id: saveWindowGeometryTimer + interval: 1000 + onTriggered: App.saveWindowGeometry(root) + } + + property int counter: 0 + + globalDrawer: Kirigami.GlobalDrawer { + isMenu: !Kirigami.Settings.isMobile + actions: [ + Kirigami.Action { + text: i18n("Plus One") + icon.name: "list-add" + onTriggered: root.counter += 1 + }, + Kirigami.Action { + text: i18n("About %{APPNAME}") + icon.name: "help-about" + onTriggered: root.pageStack.pushDialogLayer("qrc:/qt/qml/org/kde/%{APPNAME}/contents/ui/About.qml") + }, + Kirigami.Action { + text: i18n("Quit") + icon.name: "application-exit" + onTriggered: Qt.quit() + } + ] + } + + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + } + + pageStack.initialPage: page + + Kirigami.Page { + id: page + + title: i18n("Main Page") + + actions: [ + Kirigami.Action { + text: i18n("Plus One") + icon.name: "list-add" + tooltip: i18n("Add one to the counter") + onTriggered: root.counter += 1 + } + ] + + ColumnLayout { + width: page.width + + anchors.centerIn: parent + + Kirigami.Heading { + Layout.alignment: Qt.AlignCenter + text: root.counter === 0 ? i18n("Hello, World!") : root.counter + } + + QQC2.Button { + Layout.alignment: Qt.AlignHCenter + text: i18n("+ 1") + onClicked: root.counter += 1 + } + } + } +} diff --git a/templates/kirigami6/src/main.cpp b/templates/kirigami6/src/main.cpp new file mode 100644 index 0000000..7393934 --- /dev/null +++ b/templates/kirigami6/src/main.cpp @@ -0,0 +1,96 @@ +/* + SPDX-License-Identifier: GPL-2.0-or-later + SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> +*/ + +#include +#ifdef Q_OS_ANDROID +#include +#else +#include +#endif + +#include +#include +#include +#include +#include + +#include "app.h" +#include "version-%{APPNAMELC}.h" +#include +#include +#include + +#include "%{APPNAMELC}config.h" + +using namespace Qt::Literals::StringLiterals; + +#ifdef Q_OS_ANDROID +Q_DECL_EXPORT +#endif +int main(int argc, char *argv[]) +{ +#ifdef Q_OS_ANDROID + QGuiApplication app(argc, argv); + QQuickStyle::setStyle(QStringLiteral("org.kde.breeze")); +#else + QApplication app(argc, argv); + + // Default to org.kde.desktop style unless the user forces another style + if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) { + QQuickStyle::setStyle(u"org.kde.desktop"_s); + } +#endif + +#ifdef Q_OS_WINDOWS + if (AttachConsole(ATTACH_PARENT_PROCESS)) { + freopen("CONOUT$", "w", stdout); + freopen("CONOUT$", "w", stderr); + } + + QApplication::setStyle(QStringLiteral("breeze")); + auto font = app.font(); + font.setPointSize(10); + app.setFont(font); +#endif + + KLocalizedString::setApplicationDomain("%{APPNAMELC}"); + QCoreApplication::setOrganizationName(u"KDE"_s); + + KAboutData aboutData( + // The program name used internally. + u"%{APPNAMELC}"_s, + // A displayable program name string. + i18nc("@title", "%{APPNAME}"), + // The program version string. + QStringLiteral(%{APPNAMEUC}_VERSION_STRING), + // Short description of what the app does. + i18n("Application Description"), + // The license this code is released under. + KAboutLicense::GPL, + // Copyright Statement. + i18n("(c) %{CURRENT_YEAR}")); + aboutData.addAuthor(i18nc("@info:credit", "%{AUTHOR}"), + i18nc("@info:credit", "Maintainer"), + u"%{EMAIL}"_s, + u"https://yourwebsite.com"_s); + aboutData.setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"), i18nc("EMAIL OF TRANSLATORS", "Your emails")); + KAboutData::setApplicationData(aboutData); + QGuiApplication::setWindowIcon(QIcon::fromTheme(u"org.kde.%{APPNAMELC}"_s)); + + QQmlApplicationEngine engine; + + auto config = %{APPNAME}Config::self(); + + qmlRegisterSingletonInstance("org.kde.%{APPNAMELC}.private", 1, 0, "Config", config); + + engine.rootContext()->setContextObject(new KLocalizedContext(&engine)); + engine.loadFromModule("org.kde.%{APPNAMELC}", u"Main"); + + if (engine.rootObjects().isEmpty()) { + return -1; + } + + return app.exec(); +} diff --git a/tests/CardTest.qml b/tests/CardTest.qml new file mode 100644 index 0000000..b37837c --- /dev/null +++ b/tests/CardTest.qml @@ -0,0 +1,59 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami + +Item { + width: 600 + height: 600 + + Kirigami.Card { + width: 300 + anchors.centerIn: parent + + banner.title: "Card" + banner.titleIcon: "document-new" + banner.titleAlignment: alignCombo.currentValue + banner.source: "/usr/share/wallpapers/Next/contents/screenshot.png" + + contentItem: Label { + text: "Card Contents" + } + + actions: [ + Kirigami.Action { + icon.name: "document-new" + text: "Action 1" + }, + Kirigami.Action { + icon.name: "document-new" + text: "Action 2" + } + ] + } + + RowLayout { + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + + ComboBox { + id: alignCombo + + model: [ + { text: "Top Left", align: Qt.AlignLeft | Qt.AlignTop }, + { text: "Top Center", align: Qt.AlignHCenter | Qt.AlignTop }, + { text: "Top Right", align: Qt.AlignRight | Qt.AlignTop }, + { text: "Center Left", align: Qt.AlignLeft | Qt.AlignVCenter }, + { text: "Center", align: Qt.AlignHCenter | Qt.AlignVCenter }, + { text: "Center Right", align: Qt.AlignRight | Qt.AlignVCenter }, + { text: "Bottom Left", align: Qt.AlignLeft | Qt.AlignBottom }, + { text: "Bottom Center", align: Qt.AlignHCenter | Qt.AlignBottom }, + { text: "Bottom Right", align: Qt.AlignRight | Qt.AlignBottom } + ] + + textRole: "text" + valueRole: "align" + } + } +} diff --git a/tests/HeaderFooterTest.qml b/tests/HeaderFooterTest.qml new file mode 100644 index 0000000..2fd36f7 --- /dev/null +++ b/tests/HeaderFooterTest.qml @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2023 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami + +ColumnLayout { + width: 600 + height: 600 + + Text { + text: `Implicit width: ${hfLayout.implicitWidth}\nImplicit height: ${hfLayout.implicitHeight}` + } + Kirigami.HeaderFooterLayout { + id: hfLayout + Layout.fillWidth: true + Layout.fillHeight: true + header: ToolBar { + contentItem: Rectangle { + color: "red" + implicitWidth: 20 + implicitHeight: 20 + } + } + contentItem: Rectangle { + color: "lightgreen" + implicitWidth: 300 + implicitHeight: 50 + } + footer: ToolBar { + contentItem: Rectangle { + color: "blue" + height: 30 + } + } + } + Rectangle { + color: "yellow" + Layout.preferredHeight: 40 + Layout.fillWidth: true + Layout.fillHeight: true + } +} diff --git a/tests/KeyboardListTest.qml b/tests/KeyboardListTest.qml new file mode 100644 index 0000000..8ac6b23 --- /dev/null +++ b/tests/KeyboardListTest.qml @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls + +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: main + + pageStack.initialPage: Kirigami.ScrollablePage { + ListView { + model: 10 + delegate: Rectangle { + width: 100 + height: 30 + color: "white" + border.color: ListView.isCurrentItem ? "#1EA8F7" : "transparent" + border.width: 4 + radius: 4 + } + } + } +} diff --git a/tests/KeyboardTest.qml b/tests/KeyboardTest.qml new file mode 100644 index 0000000..8546149 --- /dev/null +++ b/tests/KeyboardTest.qml @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls + +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: main + + Component { + id: keyPage + Kirigami.Page { + id: page + + // Don't remove, used in autotests + readonly property alias lastKey: see.text + + Label { + id: see + anchors.centerIn: parent + color: page.activeFocus ? Kirigami.Theme.focusColor : Kirigami.Theme.textColor + } + + Keys.onPressed: event => { + if (event.text) { + see.text = event.text + } else { + see.text = event.key + } + } + + Keys.onEnterPressed: main.showPassiveNotification("page!") + } + } + + header: Label { + padding: Kirigami.Units.largeSpacing + text: `focus: ${main.activeFocusItem}, current: ${main.pageStack.currentIndex}` + } + + Component.onCompleted: { + main.pageStack.push(keyPage) + main.pageStack.push(keyPage) + } +} diff --git a/tests/ListItemTest.qml b/tests/ListItemTest.qml new file mode 100644 index 0000000..e4b1f4e --- /dev/null +++ b/tests/ListItemTest.qml @@ -0,0 +1,469 @@ +/* + * SPDX-FileCopyrightText: 2021 Nate Graham + * SPDX-FileCopyrightText: 2023 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 + +import org.kde.kirigami as Kirigami +import org.kde.kirigami.delegates as KD + +Kirigami.ApplicationWindow { + GridLayout { + anchors.fill: parent + anchors.margins: Kirigami.Units.gridUnit + + rows: 3 + rowSpacing: Kirigami.Units.gridUnit + columns: 3 + columnSpacing: Kirigami.Units.gridUnit + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.View + + // Icon + Label + ColumnLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + Kirigami.Heading { + text: "Icon + Label" + level: 3 + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + KD.SubtitleDelegate { + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + } + KD.CheckSubtitleDelegate { + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + } + KD.RadioSubtitleDelegate { + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + } + KD.SwitchSubtitleDelegate { + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + } + } + + // Label + space reserved for icon + ColumnLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + Kirigami.Heading { + text: "Icon + Label + space reserved for icon" + level: 3 + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + KD.SubtitleDelegate { + Layout.fillWidth: true + text: "Boom!" + icon.width: Kirigami.Units.iconSizes.smallMedium + } + KD.CheckSubtitleDelegate { + Layout.fillWidth: true + text: "Boom!" + icon.width: Kirigami.Units.iconSizes.smallMedium + } + KD.RadioSubtitleDelegate { + Layout.fillWidth: true + text: "Boom!" + icon.width: Kirigami.Units.iconSizes.smallMedium + } + KD.SwitchSubtitleDelegate { + Layout.fillWidth: true + text: "Boom!" + icon.width: Kirigami.Units.iconSizes.smallMedium + } + } + + // Icon + Label + leading and trailing items + ColumnLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + Kirigami.Heading { + text: "Icon + Label + leading and trailing items" + level: 3 + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + QQC2.ItemDelegate { + id: plainDelegate + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + + contentItem: RowLayout { + spacing: Kirigami.Units.smallSpacing + Rectangle { + radius: height + Layout.preferredWidth: Kirigami.Units.largeSpacing + Layout.preferredHeight: Kirigami.Units.largeSpacing + color: Kirigami.Theme.neutralTextColor + } + + KD.IconTitleSubtitle { + Layout.fillWidth: true + title: plainDelegate.text + icon: icon.fromControlsIcon(plainDelegate.icon) + } + + QQC2.Button { + icon.name: "edit-delete" + text: "Defuse the bomb!" + } + } + } + QQC2.CheckDelegate { + id: checkDelegate + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + + contentItem: RowLayout { + spacing: Kirigami.Units.smallSpacing + Rectangle { + radius: height + Layout.preferredWidth: Kirigami.Units.largeSpacing + Layout.preferredHeight: Kirigami.Units.largeSpacing + color: Kirigami.Theme.neutralTextColor + } + + KD.IconTitleSubtitle { + Layout.fillWidth: true + title: checkDelegate.text + icon: icon.fromControlsIcon(checkDelegate.icon) + } + + QQC2.Button { + icon.name: "edit-delete" + text: "Defuse the bomb!" + } + } + } + QQC2.RadioDelegate { + id: radioDelegate + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + + contentItem: RowLayout { + spacing: Kirigami.Units.smallSpacing + Rectangle { + radius: height + Layout.preferredWidth: Kirigami.Units.largeSpacing + Layout.preferredHeight: Kirigami.Units.largeSpacing + color: Kirigami.Theme.neutralTextColor + } + + KD.IconTitleSubtitle { + Layout.fillWidth: true + title: radioDelegate.text + icon: icon.fromControlsIcon(radioDelegate.icon) + } + + QQC2.Button { + icon.name: "edit-delete" + text: "Defuse the bomb!" + } + } + } + QQC2.SwitchDelegate { + id: switchDelegate + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + + contentItem: RowLayout { + spacing: Kirigami.Units.smallSpacing + Rectangle { + radius: height + Layout.preferredWidth: Kirigami.Units.largeSpacing + Layout.preferredHeight: Kirigami.Units.largeSpacing + color: Kirigami.Theme.neutralTextColor + } + + KD.IconTitleSubtitle { + Layout.fillWidth: true + title: switchDelegate.text + icon: icon.fromControlsIcon(switchDelegate.icon) + } + + QQC2.Button { + icon.name: "edit-delete" + text: "Defuse the bomb!" + } + } + } + } + + // Icon + Label + subtitle + ColumnLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + Kirigami.Heading { + text: "Icon + Label + subtitle" + level: 3 + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + KD.SubtitleDelegate { + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + subtitle: "smaller boom" + } + KD.CheckSubtitleDelegate { + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + subtitle: "smaller boom" + } + KD.RadioSubtitleDelegate { + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + subtitle: "smaller boom" + } + KD.SwitchSubtitleDelegate { + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + subtitle: "smaller boom" + } + } + + // Icon + Label + space reserved for subtitle + ColumnLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + Kirigami.Heading { + text: "Icon + Label + space reserved for subtitle" + level: 3 + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + KD.SubtitleDelegate { + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + + contentItem: KD.IconTitleSubtitle { + title: parent.text + icon: icon.fromControlsIcon(parent.icon) + reserveSpaceForSubtitle: true + } + } + KD.CheckSubtitleDelegate { + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + + contentItem: KD.IconTitleSubtitle { + title: parent.text + icon: icon.fromControlsIcon(parent.icon) + reserveSpaceForSubtitle: true + } + } + KD.RadioSubtitleDelegate { + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + + contentItem: KD.IconTitleSubtitle { + title: parent.text + icon: icon.fromControlsIcon(parent.icon) + reserveSpaceForSubtitle: true + } + } + KD.SwitchSubtitleDelegate { + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + + contentItem: KD.IconTitleSubtitle { + title: parent.text + icon: icon.fromControlsIcon(parent.icon) + reserveSpaceForSubtitle: true + } + } + } + + // Icon + Label + subtitle + leading and trailing items + ColumnLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + Kirigami.Heading { + text: "Icon + Label + subtitle + leading/trailing" + level: 3 + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + KD.SubtitleDelegate { + id: subtitleDelegate + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + subtitle: "smaller boom" + + contentItem: RowLayout { + spacing: Kirigami.Units.smallSpacing + Rectangle { + radius: height + Layout.preferredWidth: Kirigami.Units.largeSpacing + Layout.preferredHeight: Kirigami.Units.largeSpacing + color: Kirigami.Theme.neutralTextColor + } + + KD.IconTitleSubtitle { + Layout.fillWidth: true + title: subtitleDelegate.text + subtitle: subtitleDelegate.subtitle + selected: subtitleDelegate.highlighted || subtitleDelegate.down + icon: icon.fromControlsIcon(subtitleDelegate.icon) + } + + QQC2.Button { + icon.name: "edit-delete" + text: "Defuse the bomb!" + } + } + } + KD.CheckSubtitleDelegate { + id: subtitleCheckDelegate + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + subtitle: "smaller boom" + + contentItem: RowLayout { + spacing: Kirigami.Units.smallSpacing + Rectangle { + radius: height + Layout.preferredWidth: Kirigami.Units.largeSpacing + Layout.preferredHeight: Kirigami.Units.largeSpacing + color: Kirigami.Theme.neutralTextColor + } + + KD.IconTitleSubtitle { + Layout.fillWidth: true + title: subtitleCheckDelegate.text + subtitle: subtitleCheckDelegate.subtitle + selected: subtitleCheckDelegate.highlighted || subtitleCheckDelegate.down + icon: icon.fromControlsIcon(subtitleCheckDelegate.icon) + } + + QQC2.Button { + icon.name: "edit-delete" + text: "Defuse the bomb!" + } + } + } + KD.RadioSubtitleDelegate { + id: subtitleRadioDelegate + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + subtitle: "smaller boom" + + contentItem: RowLayout { + spacing: Kirigami.Units.smallSpacing + Rectangle { + radius: height + Layout.preferredWidth: Kirigami.Units.largeSpacing + Layout.preferredHeight: Kirigami.Units.largeSpacing + color: Kirigami.Theme.neutralTextColor + } + + KD.IconTitleSubtitle { + Layout.fillWidth: true + title: subtitleRadioDelegate.text + subtitle: subtitleRadioDelegate.subtitle + selected: subtitleRadioDelegate.highlighted || subtitleRadioDelegate.down + icon: icon.fromControlsIcon(subtitleRadioDelegate.icon) + } + + QQC2.Button { + icon.name: "edit-delete" + text: "Defuse the bomb!" + } + } + } + KD.SwitchSubtitleDelegate { + id: subtitleSwitchDelegate + Layout.fillWidth: true + + icon.name: "edit-bomb" + text: "Boom!" + subtitle: "smaller boom" + + contentItem: RowLayout { + spacing: Kirigami.Units.smallSpacing + Rectangle { + radius: height + Layout.preferredWidth: Kirigami.Units.largeSpacing + Layout.preferredHeight: Kirigami.Units.largeSpacing + color: Kirigami.Theme.neutralTextColor + } + + KD.IconTitleSubtitle { + Layout.fillWidth: true + title: subtitleSwitchDelegate.text + subtitle: subtitleSwitchDelegate.subtitle + selected: subtitleSwitchDelegate.highlighted || subtitleSwitchDelegate.down + icon: icon.fromControlsIcon(subtitleSwitchDelegate.icon) + } + + QQC2.Button { + icon.name: "edit-delete" + text: "Defuse the bomb!" + } + } + } + } + } +} + diff --git a/tests/NavigationTabBarTest.qml b/tests/NavigationTabBarTest.qml new file mode 100644 index 0000000..5ce2d77 --- /dev/null +++ b/tests/NavigationTabBarTest.qml @@ -0,0 +1,95 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 + +import org.kde.kirigami as Kirigami + +QQC2.ApplicationWindow { + width: 640 + height: 480 + visible: true + + QQC2.SwipeView { + id: swipeView + + anchors.fill: parent + currentIndex: navTabBar.currentIndex + + QQC2.Page { + contentItem: QQC2.Label { + text: "page1" + horizontalAlignment: Text.AlignHCenter + } + } + QQC2.Page { + contentItem: QQC2.Label { + text: "page2" + horizontalAlignment: Text.AlignHCenter + } + } + QQC2.Page { + contentItem: QQC2.Label { + text: "page3" + horizontalAlignment: Text.AlignHCenter + } + } + QQC2.Page { + contentItem: QQC2.Label { + text: "page4" + horizontalAlignment: Text.AlignHCenter + } + } + onCurrentIndexChanged: navTabBar.currentIndex = swipeView.currentIndex + } + + footer: Kirigami.NavigationTabBar { + id: navTabBar + + currentIndex: swipeView.currentIndex + + Kirigami.NavigationTabButton { + visible: true + width: navTabBar.buttonWidth + icon.name: "document-save" + text: `test ${tabIndex + 1}` + QQC2.ButtonGroup.group: navTabBar.tabGroup + } + Kirigami.NavigationTabButton { + visible: false + width: navTabBar.buttonWidth + icon.name: "document-send" + text: `test ${tabIndex + 1}` + QQC2.ButtonGroup.group: navTabBar.tabGroup + } + actions: [ + Kirigami.Action { + visible: true + icon.name: "edit-copy" + icon.height: 32 + icon.width: 32 + text: "test 3" + checked: true + }, + Kirigami.Action { + visible: true + icon.name: "edit-cut" + text: "test 4" + checkable: true + }, + Kirigami.Action { + visible: false + icon.name: "edit-paste" + text: "test 5" + }, + Kirigami.Action { + visible: true + icon.source: "../logo.png" + text: "test 6" + checkable: true + } + ] + } +} diff --git a/tests/OverlayFocusTest.qml b/tests/OverlayFocusTest.qml new file mode 100644 index 0000000..2bb1305 --- /dev/null +++ b/tests/OverlayFocusTest.qml @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2021 Ismael Asensio + * SPDX-FileCopyrightText: 2021 David Edmundson + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 + +import org.kde.kirigami as Kirigami + +Rectangle { + id: background + + implicitWidth: 600 + implicitHeight: 600 + color: Kirigami.Theme.backgroundColor + + Kirigami.FormLayout { + id: layout + anchors.centerIn: parent + + QQC2.Button { + Layout.fillWidth: true + text: "Open overlay sheet" + onClicked: sheet.open() + } + } + + Kirigami.OverlaySheet { + id: sheet + parent: background + + header: QQC2.TextField { + id: headerText + focus: true + } + footer: QQC2.TextField { + id: footerText + } + + ListView { + id: content + model: 10 + + delegate: QQC2.ItemDelegate { + text: "Item " + modelData + width: parent.width + } + } + } +} diff --git a/tests/OverlayTest.qml b/tests/OverlayTest.qml new file mode 100644 index 0000000..8a3536d --- /dev/null +++ b/tests/OverlayTest.qml @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2022 Aleix Pol + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 + +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + height: 720 + width: 360 + visible: true + + Kirigami.OverlaySheet { + id: sheet + + title: "Certificate Viewer" + + ColumnLayout { + QQC2.DialogButtonBox { + Layout.fillWidth: true + + QQC2.Button { + QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.ActionRole + text: "Export…" + } + + QQC2.Button { + QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.DestructiveRole + text: "Close" + icon.name: "dialog-close" + } + } + } + } + + Timer { + interval: 150 + running: true + onTriggered: sheet.open() + } +} diff --git a/tests/ShadowedImageTest.qml b/tests/ShadowedImageTest.qml new file mode 100644 index 0000000..3daedcd --- /dev/null +++ b/tests/ShadowedImageTest.qml @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls + +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + width: 600 + height: 800 + visible: true + + pageStack.initialPage: Kirigami.Page { + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + Column { + anchors.centerIn: parent + + Kirigami.ShadowedImage { + width: 400 + height: 300 + + color: Kirigami.Theme.highlightColor + + source: "/usr/share/wallpapers/Next/contents/images/1024x768.jpg" + + radius: radiusSlider.value + + shadow.size: sizeSlider.value + shadow.xOffset: xOffsetSlider.value + shadow.yOffset: yOffsetSlider.value + + border.width: borderWidthSlider.value + border.color: Kirigami.Theme.textColor + + corners.topLeftRadius: topLeftSlider.value + corners.topRightRadius: topRightSlider.value + corners.bottomLeftRadius: bottomLeftSlider.value + corners.bottomRightRadius: bottomRightSlider.value + } + + Kirigami.FormLayout { + Item { Kirigami.FormData.isSection: true } + + Slider { id: radiusSlider; from: 0; to: 200; Kirigami.FormData.label: "Overall Radius" } + Slider { id: topLeftSlider; from: -1; to: 200; value: -1; Kirigami.FormData.label: "Top Left Radius" } + Slider { id: topRightSlider; from: -1; to: 200; value: -1; Kirigami.FormData.label: "Top Right Radius" } + Slider { id: bottomLeftSlider; from: -1; to: 200; value: -1; Kirigami.FormData.label: "Bottom Left Radius" } + Slider { id: bottomRightSlider; from: -1; to: 200; value: -1; Kirigami.FormData.label: "Bottom Right Radius" } + + Slider { id: sizeSlider; from: 0; to: 100; Kirigami.FormData.label: "Shadow Size" } + Slider { id: xOffsetSlider; from: -100; to: 100; Kirigami.FormData.label: "Shadow X-Offset" } + Slider { id: yOffsetSlider; from: -100; to: 100; Kirigami.FormData.label: "Shadow Y-Offset" } + + Slider { id: borderWidthSlider; from: 0; to: 50; Kirigami.FormData.label: "Border Width" } + } + } + } +} diff --git a/tests/ShadowedRectangleTest.qml b/tests/ShadowedRectangleTest.qml new file mode 100644 index 0000000..8877eb0 --- /dev/null +++ b/tests/ShadowedRectangleTest.qml @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls + +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + width: 600 + height: 800 + visible: true + + pageStack.initialPage: Kirigami.Page { + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + Column { + anchors.centerIn: parent + + Kirigami.ShadowedRectangle { + width: 400 + height: 300 + + color: Kirigami.Theme.highlightColor + + radius: radiusSlider.value + + shadow.size: sizeSlider.value + shadow.xOffset: xOffsetSlider.value + shadow.yOffset: yOffsetSlider.value + + border.width: borderWidthSlider.value + border.color: Kirigami.Theme.textColor + + corners.topLeftRadius: topLeftSlider.value + corners.topRightRadius: topRightSlider.value + corners.bottomLeftRadius: bottomLeftSlider.value + corners.bottomRightRadius: bottomRightSlider.value + } + + Kirigami.FormLayout { + Item { Kirigami.FormData.isSection: true } + + Slider { id: radiusSlider; from: 0; to: 200; Kirigami.FormData.label: "Overall Radius" } + Slider { id: topLeftSlider; from: -1; to: 200; value: -1; Kirigami.FormData.label: "Top Left Radius" } + Slider { id: topRightSlider; from: -1; to: 200; value: -1; Kirigami.FormData.label: "Top Right Radius" } + Slider { id: bottomLeftSlider; from: -1; to: 200; value: -1; Kirigami.FormData.label: "Bottom Left Radius" } + Slider { id: bottomRightSlider; from: -1; to: 200; value: -1; Kirigami.FormData.label: "Bottom Right Radius" } + + Slider { id: sizeSlider; from: 0; to: 100; Kirigami.FormData.label: "Shadow Size" } + Slider { id: xOffsetSlider; from: -100; to: 100; Kirigami.FormData.label: "Shadow X-Offset" } + Slider { id: yOffsetSlider; from: -100; to: 100; Kirigami.FormData.label: "Shadow Y-Offset" } + + Slider { id: borderWidthSlider; from: 0; to: 50; Kirigami.FormData.label: "Border Width" } + } + } + } +} diff --git a/tests/actionsMenu.qml b/tests/actionsMenu.qml new file mode 100644 index 0000000..7023563 --- /dev/null +++ b/tests/actionsMenu.qml @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: main + + pageStack.initialPage: Kirigami.Page { + QQC2.Button { + text: "button" + onClicked: menu.popup() + QQC2.Menu { + id: menu + + QQC2.MenuItem { text: "xxx" } + QQC2.MenuItem { text: "xxx" } + QQC2.Menu { + title: "yyy" + QQC2.MenuItem { text: "yyy" } + QQC2.MenuItem { text: "yyy" } + } + } + } + + title: "aaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaa" + + QQC2.ActionGroup { + id: group + } + + actions: [ + Kirigami.Action { + text: "submenus" + icon.name: "kalgebra" + + Kirigami.Action { text: "xxx"; onTriggered: console.log("xxx") } + Kirigami.Action { text: "xxx"; onTriggered: console.log("xxx") } + Kirigami.Action { text: "xxx"; onTriggered: console.log("xxx") } + Kirigami.Action { + text: "yyy" + Kirigami.Action { text: "yyy" } + Kirigami.Action { text: "yyy" } + Kirigami.Action { text: "yyy" } + Kirigami.Action { text: "yyy" } + } + }, + Kirigami.Action { + id: optionsAction + text: "Options" + icon.name: "kate" + + Kirigami.Action { + QQC2.ActionGroup.group: group + text: "A" + checkable: true + checked: true + } + Kirigami.Action { + QQC2.ActionGroup.group: group + text: "B" + checkable: true + } + Kirigami.Action { + QQC2.ActionGroup.group: group + text: "C" + checkable: true + } + }, + Kirigami.Action { text: "stuffing..." }, + Kirigami.Action { text: "stuffing..." }, + Kirigami.Action { text: "stuffing..." }, + Kirigami.Action { text: "stuffing..." }, + Kirigami.Action { text: "stuffing..." } + ] + } +} diff --git a/tests/cardsList.qml b/tests/cardsList.qml new file mode 100644 index 0000000..23a3379 --- /dev/null +++ b/tests/cardsList.qml @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls + +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + + Component { + id: delegateComponent + Kirigami.Card { + contentItem: Label { text: ourlist.prefix + index } + } + } + + pageStack.initialPage: Kirigami.ScrollablePage { + + Kirigami.CardsListView { + id: ourlist + property string prefix: "ciao " + + delegate: delegateComponent + + model: 100 + } + } +} diff --git a/tests/iconTest.qml b/tests/iconTest.qml new file mode 100644 index 0000000..423336f --- /dev/null +++ b/tests/iconTest.qml @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + + Component { + id: delegateComponent + Kirigami.Card { + contentItem: Label { text: ourlist.prefix + index } + } + } + + pageStack.initialPage: Kirigami.Page { + actions: [ + Kirigami.Action { + text: "Switch Icon" + onTriggered: { + if (icon.source === "home") { + icon.source = "window-new"; + } else { + icon.source = "home"; + } + } + }, + Kirigami.Action { + text: "Enabled" + checkable: true + checked: icon.enabled + onTriggered: icon.enabled = !icon.enabled + }, + Kirigami.Action { + text: "Animated" + checkable: true + checked: icon.animated + onTriggered: icon.animated = !icon.animated + }, + Kirigami.Action { + displayComponent: RowLayout { + Label { + text: "Size:" + } + SpinBox { + from: 0 + to: Kirigami.Units.iconSizes.enormous + value: Kirigami.Units.iconSizes.large + onValueModified: { + icon.width = value; + icon.height = value; + } + } + } + } + ] + + Kirigami.Icon { + id: icon + width: Kirigami.Units.iconSizes.Large + height: width + source: "home" + } + } +} diff --git a/tests/swipeListItemTest.qml b/tests/swipeListItemTest.qml new file mode 100644 index 0000000..14cf0e7 --- /dev/null +++ b/tests/swipeListItemTest.qml @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls + +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: main + + pageStack.initialPage: Kirigami.ScrollablePage { + ListView { + model: 25 + delegate: Kirigami.SwipeListItem { + supportsMouseEvents: false + actions: [ + Kirigami.Action { + icon.name: "go-up" + } + ] + contentItem: Label { + elide: Text.ElideRight + text: "big banana big banana big banana big banana big banana big banana big banana big banana big banana big banana big banana big banana big banana big banana big banana big banana big banana big banana big banana big banana" + } + } + } + } +} diff --git a/tests/wheelhandler/ScrollView.qml b/tests/wheelhandler/ScrollView.qml new file mode 100644 index 0000000..5505ee7 --- /dev/null +++ b/tests/wheelhandler/ScrollView.qml @@ -0,0 +1,46 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls as QQC2 + +import org.kde.kirigami as Kirigami + +T.ScrollView { + id: control + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + contentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + contentHeight + topPadding + bottomPadding) + + leftPadding: mirrored && T.ScrollBar.vertical && T.ScrollBar.vertical.visible && !Kirigami.Settings.isMobile ? T.ScrollBar.vertical.width : 0 + rightPadding: !mirrored && T.ScrollBar.vertical && T.ScrollBar.vertical.visible && !Kirigami.Settings.isMobile ? T.ScrollBar.vertical.width : 0 + bottomPadding: T.ScrollBar.horizontal && T.ScrollBar.horizontal.visible && !Kirigami.Settings.isMobile ? T.ScrollBar.horizontal.height : 0 + + data: [ + Kirigami.WheelHandler { + id: wheelHandler + target: control.contentItem + } + ] + + T.ScrollBar.vertical: QQC2.ScrollBar { + parent: control + x: control.mirrored ? 0 : control.width - width + y: control.topPadding + height: control.availableHeight + active: control.T.ScrollBar.horizontal.active + stepSize: wheelHandler.verticalStepSize / control.contentHeight + } + + T.ScrollBar.horizontal: QQC2.ScrollBar { + parent: control + x: control.leftPadding + y: control.height - height + width: control.availableWidth + active: control.T.ScrollBar.vertical.active + stepSize: wheelHandler.horizontalStepSize / control.contentWidth + } +} diff --git a/tests/wheelhandler/WheelHandlerFlickableTest.qml b/tests/wheelhandler/WheelHandlerFlickableTest.qml new file mode 100644 index 0000000..4254bb8 --- /dev/null +++ b/tests/wheelhandler/WheelHandlerFlickableTest.qml @@ -0,0 +1,100 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick +import QtQuick.Controls as QQC2 + +import org.kde.kirigami as Kirigami + +QQC2.ApplicationWindow { + id: root + width: flickable.implicitWidth + height: flickable.implicitHeight + visible: true + + Flickable { + id: flickable + anchors.fill: parent + implicitWidth: wheelHandler.horizontalStepSize * 10 + leftMargin + rightMargin + implicitHeight: wheelHandler.verticalStepSize * 10 + topMargin + bottomMargin + + leftMargin: QQC2.ScrollBar.vertical.visible && QQC2.ScrollBar.vertical.mirrored ? QQC2.ScrollBar.vertical.width : 0 + rightMargin: QQC2.ScrollBar.vertical.visible && !QQC2.ScrollBar.vertical.mirrored ? QQC2.ScrollBar.vertical.width : 0 + bottomMargin: QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.height : 0 + + contentWidth: contentItem.childrenRect.width + contentHeight: contentItem.childrenRect.height + + Kirigami.WheelHandler { + id: wheelHandler + target: flickable + filterMouseEvents: true + keyNavigationEnabled: true + } + + QQC2.ScrollBar.vertical: QQC2.ScrollBar { + parent: flickable.parent + height: flickable.height - flickable.topMargin - flickable.bottomMargin + x: mirrored ? 0 : flickable.width - width + y: flickable.topMargin + active: flickable.QQC2.ScrollBar.horizontal.active + stepSize: wheelHandler.verticalStepSize / flickable.contentHeight + } + + QQC2.ScrollBar.horizontal: QQC2.ScrollBar { + parent: flickable.parent + width: flickable.width - flickable.leftMargin - flickable.rightMargin + x: flickable.leftMargin + y: flickable.height - height + active: flickable.QQC2.ScrollBar.vertical.active + stepSize: wheelHandler.horizontalStepSize / flickable.contentWidth + } + + Grid { + columns: Math.sqrt(visibleChildren.length) + Repeater { + model: 500 + delegate: Rectangle { + implicitWidth: wheelHandler.horizontalStepSize + implicitHeight: wheelHandler.verticalStepSize + gradient: Gradient { + orientation: index % 2 ? Gradient.Vertical : Gradient.Horizontal + GradientStop { position: 0; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) } + GradientStop { position: 1; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) } + } + } + } + QQC2.Button { + id: enableSliderButton + width: wheelHandler.horizontalStepSize + height: wheelHandler.verticalStepSize + contentItem: QQC2.Label { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: "Enable Slider" + wrapMode: Text.Wrap + } + checked: true + } + QQC2.Slider { + id: slider + enabled: enableSliderButton.checked + width: wheelHandler.horizontalStepSize + height: wheelHandler.verticalStepSize + } + Repeater { + model: 500 + delegate: Rectangle { + implicitWidth: wheelHandler.horizontalStepSize + implicitHeight: wheelHandler.verticalStepSize + gradient: Gradient { + orientation: index % 2 ? Gradient.Vertical : Gradient.Horizontal + GradientStop { position: 0; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) } + GradientStop { position: 1; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) } + } + } + } + } + } +} diff --git a/tests/wheelhandler/WheelHandlerScrollViewTest.qml b/tests/wheelhandler/WheelHandlerScrollViewTest.qml new file mode 100644 index 0000000..9a347fe --- /dev/null +++ b/tests/wheelhandler/WheelHandlerScrollViewTest.qml @@ -0,0 +1,66 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick +import QtQml +import QtQuick.Templates as T +import QtQuick.Controls as QQC2 + +import org.kde.kirigami as Kirigami + +QQC2.ApplicationWindow { + id: root + width: 200 * Qt.styleHints.wheelScrollLines + scrollView.leftPadding + scrollView.rightPadding + height: 200 * Qt.styleHints.wheelScrollLines + scrollView.topPadding + scrollView.bottomPadding + visible: true + ScrollView { + id: scrollView + anchors.fill: parent + Grid { + columns: Math.sqrt(visibleChildren.length) + Repeater { + model: 500 + delegate: Rectangle { + implicitWidth: 20 * Qt.styleHints.wheelScrollLines + implicitHeight: 20 * Qt.styleHints.wheelScrollLines + gradient: Gradient { + orientation: index % 2 ? Gradient.Vertical : Gradient.Horizontal + GradientStop { position: 0; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) } + GradientStop { position: 1; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) } + } + } + } + QQC2.Button { + id: enableSliderButton + width: 20 * Qt.styleHints.wheelScrollLines + height: 20 * Qt.styleHints.wheelScrollLines + contentItem: QQC2.Label { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: "Enable Slider" + wrapMode: Text.Wrap + } + checked: true + } + QQC2.Slider { + id: slider + enabled: enableSliderButton.checked + width: 20 * Qt.styleHints.wheelScrollLines + height: 20 * Qt.styleHints.wheelScrollLines + } + Repeater { + model: 500 + delegate: Rectangle { + implicitWidth: 20 * Qt.styleHints.wheelScrollLines + implicitHeight: 20 * Qt.styleHints.wheelScrollLines + gradient: Gradient { + orientation: index % 2 ? Gradient.Vertical : Gradient.Horizontal + GradientStop { position: 0; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) } + GradientStop { position: 1; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) } + } + } + } + } + } +} diff --git a/tests/wheelhandler/WheelHandlerScrollViewTextAreaTest.qml b/tests/wheelhandler/WheelHandlerScrollViewTextAreaTest.qml new file mode 100644 index 0000000..3e8cd28 --- /dev/null +++ b/tests/wheelhandler/WheelHandlerScrollViewTextAreaTest.qml @@ -0,0 +1,26 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick +import QtQml +import QtQuick.Templates as T +import QtQuick.Controls as QQC2 + +import org.kde.kirigami as Kirigami + +QQC2.ApplicationWindow { + id: root + width: 600 + height: 600 + visible: true + + ScrollView { + id: scrollView + anchors.fill: parent + QQC2.TextArea { + wrapMode: TextEdit.Wrap + text: "Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + } + } +} diff --git a/tests/wheelhandler/scrollableqtextedit.ui b/tests/wheelhandler/scrollableqtextedit.ui new file mode 100644 index 0000000..45c5b0d --- /dev/null +++ b/tests/wheelhandler/scrollableqtextedit.ui @@ -0,0 +1,55 @@ + + + MainWindow + + + + 0 + 0 + 600 + 600 + + + + MainWindow + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + false + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p></body></html> + + + false + + + + + + + + +