From: Aurélien COUDERC Date: Sun, 18 Sep 2022 21:11:46 +0000 (+0100) Subject: Import kirigami2_5.98.0.orig.tar.xz X-Git-Tag: archive/raspbian/5.98.0-1+rpi1^2~2 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=1c7d0eefcf4b17cc2e5acc664386cfd8d91849b1;p=kirigami2.git Import kirigami2_5.98.0.orig.tar.xz [dgit import orig kirigami2_5.98.0.orig.tar.xz] --- 1c7d0eefcf4b17cc2e5acc664386cfd8d91849b1 diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..c72c528 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +#clang-tidy +ae47f7f9553bd222123acc77f336753840173c57 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dcac9c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# 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*/ +CMakeLists.txt.user* +*.unc-backup* +.cmake/ +.vscode/ +/.clang-format +/compile_commands.json +.clangd +.idea +/cmake-build* +.cache diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..be40e90 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2020 Volker Krause +# SPDX-License-Identifier: CC0-1.0 + +include: + - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml + - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml + - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml + - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml + - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml + - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml + +suse_tumbleweed_qt515_static: + extends: suse_tumbleweed_qt515 + script: python3 -u ci-utilities/run-ci-build.py --project $CI_PROJECT_NAME --branch $CI_COMMIT_REF_NAME --platform Linux --extra-cmake-args="-DBUILD_SHARED_LIBS=OFF" --skip-publishing + diff --git a/.kde-ci.yml b/.kde-ci.yml new file mode 100644 index 0000000..3580259 --- /dev/null +++ b/.kde-ci.yml @@ -0,0 +1,8 @@ +Dependencies: +- 'on': ['Linux', 'FreeBSD', 'Windows', 'Android', 'iOS'] + 'require': + 'frameworks/extra-cmake-modules': '@same' + +Options: + test-before-installing: True + require-passing-tests-on: [ 'FreeBSD' ] diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f6926d8 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,160 @@ +cmake_minimum_required(VERSION 3.16) + +set(KF_VERSION "5.98.0") # handled by release scripts +set(KF_DEP_VERSION "5.98.0") # handled by release scripts + +project(kirigami2 VERSION ${KF_VERSION}) + +set(REQUIRED_QT_VERSION 5.15.2) + +################# 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(DISABLE_DBUS "Build without D-Bus support" 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 5.98.0 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() + +# Make CPack available to easy generate binary packages +include(CPack) +include(FeatureSummary) +include(KDEInstallDirs) +find_package(Qt${QT_MAJOR_VERSION} ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE COMPONENTS Core Quick Gui Svg QuickControls2 Concurrent) +if (BUILD_TESTING) + find_package(Qt${QT_MAJOR_VERSION}QuickTest ${REQUIRED_QT_VERSION} CONFIG QUIET) +endif() +get_target_property(QtGui_Enabled_Features Qt${QT_MAJOR_VERSION}::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(ECMGenerateQmlTypes) +include(ECMQmlModule) +include(ECMDeprecationSettings) + +set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF5Kirigami2") + +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)") + +configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/KF5Kirigami2Config.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/KF5Kirigami2Config.cmake" + INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} + PATH_VARS CMAKE_INSTALL_PREFIX +) + +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/KF5Kirigami2Config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/KF5Kirigami2ConfigVersion.cmake" + "${CMAKE_CURRENT_SOURCE_DIR}/KF5Kirigami2Macros.cmake" + DESTINATION "${CMAKECONFIG_INSTALL_DIR}" + COMPONENT Devel +) + +install(EXPORT KF5Kirigami2Targets + DESTINATION "${CMAKECONFIG_INSTALL_DIR}" + FILE KF5Kirigami2Targets.cmake + NAMESPACE KF5:: +) + + +ecm_setup_version(${KF_VERSION} + VARIABLE_PREFIX KIRIGAMI2 + VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/kirigami_version.h" + PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF5Kirigami2ConfigVersion.cmake" + SOVERSION 5 +) + +#use dbus on linux, bsd etc, but not android and apple stuff +if (UNIX AND NOT ANDROID AND NOT(APPLE) AND NOT(DISABLE_DBUS)) + find_package(Qt${QT_MAJOR_VERSION}DBus) + add_definitions(-DKIRIGAMI_ENABLE_DBUS) +endif() + +if (UBUNTU_TOUCH) + add_definitions(-DUBUNTU_TOUCH) +endif() + +if(BUILD_EXAMPLES AND ANDROID) + find_package(Qt${QT_MAJOR_VERSION}AndroidExtras ${REQUIRED_QT_VERSION} REQUIRED) +endif() + + +ecm_find_qmlmodule(QtGraphicalEffects 1.0) + +ecm_set_disabled_deprecation_versions( + QT 5.15.2 + KF 5.95 +) + +# temporary Qt6 porting scafolding until branching +# (this is intentional made to fail if we forget it for KF6) +if (TARGET Qt6::Core AND KF_VERSION VERSION_LESS 6.0) + message(WARNING "TODO: port shadow shaders to Qt6!") + set(KF6_PORTING_TODO TRUE) + add_definitions(-DKF6_PORTING_TODO) +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() + +ecm_install_po_files_as_qm(po) + +feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) + +kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) diff --git a/KF5Kirigami2Config.cmake.in b/KF5Kirigami2Config.cmake.in new file mode 100644 index 0000000..ce42cdf --- /dev/null +++ b/KF5Kirigami2Config.cmake.in @@ -0,0 +1,15 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(Qt@QT_MAJOR_VERSION@Core @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}/KF5Kirigami2Targets.cmake") + +set(Kirigami2_INSTALL_PREFIX "@PACKAGE_CMAKE_INSTALL_PREFIX@") + +#set(Kirigami2_LIBRARIES KF5::Kirigami2) + +include("${CMAKE_CURRENT_LIST_DIR}/KF5Kirigami2Macros.cmake") +@PACKAGE_INCLUDE_QCHTARGETS@ diff --git a/KF5Kirigami2Macros.cmake b/KF5Kirigami2Macros.cmake new file mode 100644 index 0000000..6c0bbcf --- /dev/null +++ b/KF5Kirigami2Macros.cmake @@ -0,0 +1,95 @@ +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() + + #include icons used by Kirigami components themselves + set(ARG_ICONS ${ARG_ICONS} go-next go-previous go-up handle-sort overflow-menu-left overflow-menu-right go-next-symbolic go-next-symbolic-rtl go-previous-symbolic go-previous-symbolic-rtl overflow-menu user view-left-new view-right-new view-left-close view-right-close dialog-positive dialog-warning dialog-error dialog-information dialog-close globe mail-sent tools-report-bug) + + function(_find_breeze_icon icon varName) + #HACKY + SET(path "") + file(GLOB_RECURSE path ${_BREEZEICONS_DIR}/icons/*/48/${icon}.svg ) + + #search in other sizes as well + if (path STREQUAL "") + file(GLOB_RECURSE path ${_BREEZEICONS_DIR}/icons/*/32/${icon}.svg ) + if (path STREQUAL "") + file(GLOB_RECURSE path ${_BREEZEICONS_DIR}/icons/*/22/${icon}.svg ) + if (path STREQUAL "") + file(GLOB_RECURSE path ${_BREEZEICONS_DIR}/icons/*/16/${icon}.svg ) + endif() + endif() + endif() + if (path STREQUAL "") + file(GLOB_RECURSE path ${_BREEZEICONS_DIR}/icons/*/symbolic/${icon}.svg ) + endif() + if (path STREQUAL "") + return() + 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(path "${path}" REALPATH) + + SET(${varName} ${path} PARENT_SCOPE) + 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() + + # external projects are only pulled at make time, not configure time + # so this is too late to work with the _find_breeze_icon() method + # _find_breeze_icon() would need to be turned into a target/command + if (FALSE) + ExternalProject_Add( + breeze-icons + PREFIX breeze-icons + GIT_REPOSITORY https://invent.kde.org/frameworks/breeze-icons.git + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + LOG_DOWNLOAD ON + ) + endif() + endif() + + message (STATUS "Found external breeze icons:") + foreach(_iconName ${ARG_ICONS}) + set(_iconPath "") + _find_breeze_icon(${_iconName} _iconPath) + message (STATUS ${_iconPath}) + if (EXISTS ${_iconPath}) + install(FILES ${_iconPath} DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami.2/icons/ RENAME ${_iconName}.svg) + endif() + endforeach() + + #generate an index.theme that qiconloader can understand + file(WRITE ${CMAKE_BINARY_DIR}/index.theme "[Icon Theme]\nName=Breeze\nDirectories=icons\n[icons]\nSize=32\nType=Scalable") + install(FILES ${CMAKE_BINARY_DIR}/index.theme DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami.2/) +endfunction() + 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/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-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/Mainpage.dox b/Mainpage.dox new file mode 100644 index 0000000..3338e0e --- /dev/null +++ b/Mainpage.dox @@ -0,0 +1,205 @@ +/* + * 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 Qt Quick Controls 2. Its goal is to enable creation of applications that look and feel great on mobile as well as desktop devices and follow the KDE Human Interface Guidelines. +The target of those components is anybody that wants to do an application using QtQuick as its main UI, especially if targeting a mobile platform, without adding many dependencies. They work on a variety of platforms, such as Plasma Mobile, Desktop Linux, Android, iOS and Windows. +It is a Tier-1 KDE Framework starting with KDE Frameworks 5.37. + +\subsection tutorial Tutorial +A tutorial for Kirigami is available on our developer platform. + +\subsection components Main Components +- \link org::kde::kirigami::ApplicationWindow ApplicationWindow \endlink +- \link org::kde::kirigami::Action Action \endlink +- \link org::kde::kirigami::GlobalDrawer GlobalDrawer \endlink +- \link org::kde::kirigami::ContextDrawer ContextDrawer \endlink +- \link org::kde::kirigami::OverlayDrawer OverlayDrawer \endlink +- \link org::kde::kirigami::Page Page \endlink +- \link org::kde::kirigami::ScrollablePage ScrollablePage \endlink +- \link org::kde::kirigami::templates::OverlaySheet OverlaySheet \endlink +- \link PageRouter PageRouter \endlink +- \link PageRoute PageRoute \endlink +- \link Kirigami::PlatformTheme Theme \endlink +- \link Kirigami::Units Units \endlink +- \link Icon Icon \endlink +- \link org::kde::kirigami::AbstractApplicationHeader AbstractApplicationHeader \endlink +- \link org::kde::kirigami::AbstractApplicationWindow AbstractApplicationWindow \endlink +- \link org::kde::kirigami::AbstractListItem AbstractListItem \endlink +- \link org::kde::kirigami::BasicListItem BasicListItem \endlink +- \link org::kde::kirigami::ListSectionHeader ListSectionHeader \endlink +- \link org::kde::kirigami::SwipeListItem SwipeListItem \endlink +- \link org::kde::kirigami::Heading Heading \endlink +- \link org::kde::kirigami::Label Label \endlink +- \link org::kde::kirigami::PlaceholderMessage PlaceholderMessage \endlink +- \link org::kde::kirigami::Dialog Dialog \endlink +- \link org::kde::kirigami::NavigationTabBar NavigationTabBar \endlink + + +\subsection example Minimal Example + +@code +import QtQuick 2.1 +import QtQuick.Controls 2.0 as QQC2 +import org.kde.kirigami 2.4 as Kirigami + +Kirigami.ApplicationWindow { + id: root + + globalDrawer: Kirigami.GlobalDrawer { + title: "Hello App" + 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: "action 3" + }, + Kirigami.Action { + text: "action 4" + } + ] + } + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + } + pageStack.initialPage: mainPageComponent + Component { + id: mainPageComponent + Kirigami.ScrollablePage { + 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... + } + } +} +@endcode + +\subsection deployment Deployment +CMake is recommended for both building Kirigami and the project using it, qmake is supported as well, so we can have several configurations, depending what is the host build system and how the deployment needs to be done. +Kirigami can be built in two ways: both as a module or statically linked in 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) +- Kirigami built as a module with qmake +- Kirigami statically built with qmake (needed to link statically from applications built with qmake) + +The simplest and recommended way to use Kirigami is to just use the module provided by the Linux distribution, or build it as a module and deploy it together 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. + +If qmake needs to be used, it's recommended to follow the schema of the example app present in the folder examples/minimalqmake of the Kirigami source code. + +It will use Kirigami statically linked only on Android, while on desktop systems it will use the version provided by the distribution. What platforms use the static version and what 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 relevant part in the .pro file is: + +@code +android: { + include(3rdparty/kirigami/kirigami.pri) +} +@endcode + +While the main.cpp file will have something like: +@code +#include +#include +#ifdef Q_OS_ANDROID +#include "./3rdparty/kirigami/src/kirigamiplugin.h" +#endif + +int main(int argc, char *argv[]) +{ + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + 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/* +// 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..339258b --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +# Kirigami + +QtQuick plugins to build user interfaces based on the KDE UX guidelines + +## Introduction + +Kirigami is a set of QtQuick components at the moment targeted for mobile use (in the future desktop as well) targeting both Plasma Mobile and Android. It’s not a whole set of components, all the “Primitive” ones like buttons and textboxes are a job for QtQuickControls (soon QtQuickControls2) but it’s a set of high level components to make the creation of applications that look and feel great on mobile as well as desktop devices and follow the Kirigami Human Interface Guidelines. + +## Build examples to desktop + +Build all examples available + +```sh +mkdir build +cd build +cmake .. -DBUILD_EXAMPLES=ON +make +``` + +Then, you can run: + +```sh +./examples/applicationitemapp/applicationitemapp +# or +./examples/galleryapp/kirigami2gallery +``` + +## Build the gallery example app on Android + +Make sure to install **android-sdk**, **android-ndk** and **android-qt5-arch**, where **arch** should be the same architecture that you aim to deploy. + +```sh +mkdir build +cd build +cmake .. \ + -DQTANDROID_EXPORTED_TARGET=kirigami2gallery \ + -DBUILD_EXAMPLES=on \ + -DANDROID_APK_DIR=../examples/galleryapp \ + -DECM_DIR=/path/to/share/ECM/cmake \ + -DCMAKE_TOOLCHAIN_FILE=/usr/share/ECM/toolchain/Android.cmake \ + -DECM_ADDITIONAL_FIND_ROOT_PATH=/path/to/Qt5.7.0/5.7/{arch} \ + -DCMAKE_PREFIX_PATH=/path/to/Qt5.7.0/5.7/{arch}/path/to/Qt5Core \ + -DANDROID_NDK=/path/to/Android/Sdk/ndk-bundle \ + -DANDROID_SDK_ROOT=/path/to/Android/Sdk/ \ + -DANDROID_SDK_BUILD_TOOLS_REVISION=26.0.2 \ + -DCMAKE_INSTALL_PREFIX=/path/to/dummy/install/prefix +``` + +You need a `-DCMAKE_INSTALL_PREFIX` to somewhere in your home, but using an absolute path. + +If you have a local checkout of the breeze-icons repo, you can avoid the cloning of the build dir +by passing also `-DBREEZEICONS_DIR=/path/to/existing/sources/of/breeze-icons` + +```sh +make create-apk-kirigami2gallery +``` + +Apk will be generated at `./kirigami2gallery_build_apk/build/outputs/apk/kirigami2gallery_build_apk-debug.apk`. + +To directly install on a phone: + +```sh +adb install -r ./kirigami2gallery_build_apk/build/outputs/apk/kirigami2gallery_build_apk-debug.apk +``` + +To perform this, your device need to be configureted with `USB debugging` and `install via USB` in `Developer options`. + +> Some ambient variables must be set before the process: `ANDROID_NDK`, `ANDROID_SDK_ROOT`, `Qt5_android` and `JAVA_HOME` + +```sh +export ANDROID_NDK=/path/to/android-ndk +export ANDROID_SDK_ROOT=/path/to/android-sdk +export Qt5_android=/path/to/android-qt5/5.7.0/{arch} +export PATH=$ANDROID_SDK_ROOT/platform-tools/:$PATH +# adapt the following path to your ant installation +export ANT=/usr/bin/ant +export JAVA_HOME=/path/to/lib/jvm/java-8-openjdk/ +``` + +# Build on your application Android, ship it together Kirigami + +1) Build kirigami + + Use the same procedure mentioned above (but without `BUILD_EXAMPLES` switch): + - `cd` into kirigami sources directory; + - Execute build script: + ```sh + mkdir build + cd build + + cmake .. \ + -DCMAKE_TOOLCHAIN_FILE=/path/to/share/ECM/toolchain/Android.cmake\ + -DCMAKE_PREFIX_PATH=/path/to/Qt5.7.0/5.7/android_armv7/\ + -DCMAKE_INSTALL_PREFIX=/path/to/dummy/install/prefix\ + -DECM_DIR=/path/to/share/ECM/cmake + + make + make install + ``` + - Note: omit the `make create-apk-kirigami2gallery` step. + +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. + - `cd` into your application sources directory; + - Replace `$yourapp` with the actual name of your application; + - Execute build script: + ```sh + mkdir build + cd build + + cmake .. \ + -DCMAKE_TOOLCHAIN_FILE=/path/to/share/ECM/toolchain/Android.cmake \ + -DQTANDROID_EXPORTED_TARGET=$yourapp \ + -DANDROID_APK_DIR=../examples/galleryapp/ \ + -DCMAKE_PREFIX_PATH=/path/to/Qt5.7.0/5.7/android_armv7/ \ + -DCMAKE_INSTALL_PREFIX=/path/to/dummy/install/prefix + + make + make install + make create-apk-$yourapp + ``` + - Note: `-DCMAKE_INSTALL_PREFIX` directory will 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. + +# Build an application with qmake + +* Use `examples/minimalqmake` example as a template. +* It links statically for Android, but on desktop systems it links to the shared library provided by your distribution. However, static linking mode may be useful for other systems such as iOS or Windows. +* Static linking only: clone `kirigami` and `breeze-icons` git repositories under the 3rdparty folder. +* Android only: in your `main()` call `KirigamiPlugin::getInstance().registerTypes();` to register QML types. +* QtCreator should be able to deploy on Android out of the box via auto-detected Android Kit, provided that SDK, NDK and other relevant tools are installed. diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt new file mode 100644 index 0000000..7982d09 --- /dev/null +++ b/autotests/CMakeLists.txt @@ -0,0 +1,52 @@ +if(NOT TARGET Qt${QT_MAJOR_VERSION}::QuickTest) + message(STATUS "Qt${QT_MAJOR_VERSION}QuickTest not found, autotests will not be built.") + return() +endif() + +add_executable(qmltest qmltest.cpp) +target_link_libraries(qmltest PRIVATE Qt::Qml Qt${QT_MAJOR_VERSION}::QuickTest) + +if (NOT BUILD_SHARED_LIBS) + target_compile_definitions(qmltest PRIVATE STATIC_MODULE) + target_link_libraries(qmltest PRIVATE KirigamiPlugin) +endif() + +macro(kirigami_add_tests) + if (WIN32) + set(_extra_args -platform offscreen) + endif() + + foreach(test ${ARGV}) + add_test(NAME ${test} + COMMAND qmltest + ${_extra_args} + -import ${CMAKE_BINARY_DIR}/bin + -input ${test} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endforeach() +endmacro() + +kirigami_add_tests( + tst_keynavigation.qml + tst_listskeynavigation.qml + tst_pagerow.qml + tst_icon.qml + tst_actiontoolbar.qml + tst_pagerouter.qml + tst_routerwindow.qml + tst_avatar.qml + tst_theme.qml + tst_mnemonicdata.qml + tst_formlayout.qml + pagepool/tst_pagepool.qml + pagepool/tst_layers.qml + wheelhandler/tst_filterMouseEvents.qml + wheelhandler/tst_invokables.qml + wheelhandler/tst_onWheel.qml + wheelhandler/tst_scrolling.qml +) + +set_tests_properties(tst_theme.qml PROPERTIES + ENVIRONMENT "QT_QUICK_CONTROLS_STYLE=default;KIRIGAMI_FORCE_STYLE=1" +) diff --git a/autotests/pagepool/TestPage.qml b/autotests/pagepool/TestPage.qml new file mode 100644 index 0000000..d455405 --- /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 2.7 +import org.kde.kirigami 2.11 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..598f94c --- /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 2.7 +import QtQuick.Controls 2.7 +import QtQuick.Window 2.1 +import org.kde.kirigami 2.11 as Kirigami +import QtTest 1.0 + +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..c0378f9 --- /dev/null +++ b/autotests/pagepool/tst_pagepool.qml @@ -0,0 +1,176 @@ +/* + * SPDX-FileCopyrightText: 2020 Mason McParlane + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.7 +import QtQuick.Controls 2.0 +import QtQuick.Window 2.1 +import org.kde.kirigami 2.11 as Kirigami +import QtTest 1.0 + +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 () { + 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 () { + 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) + } + verify(pool.items.length == 5, "pool contains 5 items") + for (const item of pool.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) + } + verify(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) + } + verify(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) + } + verify(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..464ab0f --- /dev/null +++ b/autotests/qmltest.cpp @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include +#include + +#ifdef STATIC_MODULE +#include "kirigamiplugin.h" +Q_IMPORT_PLUGIN(KirigamiPlugin) +#endif + +class KirigamiSetup : public QObject +{ + Q_OBJECT + +public: + KirigamiSetup() + { + } + +public Q_SLOTS: + void qmlEngineAvailable(QQmlEngine *engine) + { +#ifdef STATIC_MODULE + KirigamiPlugin::getInstance().registerTypes(engine); +#endif + } +}; + +QUICK_TEST_MAIN_WITH_SETUP(Kirigami, KirigamiSetup) + +#include "qmltest.moc" diff --git a/autotests/tst_actiontoolbar.qml b/autotests/tst_actiontoolbar.qml new file mode 100644 index 0000000..8cb8e74 --- /dev/null +++ b/autotests/tst_actiontoolbar.qml @@ -0,0 +1,172 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtTest 1.0 +import org.kde.kirigami 2.14 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) + } +} diff --git a/autotests/tst_avatar.qml b/autotests/tst_avatar.qml new file mode 100644 index 0000000..777983c --- /dev/null +++ b/autotests/tst_avatar.qml @@ -0,0 +1,40 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 as QQC2 +import org.kde.kirigami 2.14 as Kirigami +import QtTest 1.0 + +Kirigami.PageRow { + id: root + TestCase { + name: "AvatarTests" + function test_latin_name() { + compare(Kirigami.NameUtils.isStringUnsuitableForInitials("Nate Martin"), false) + compare(Kirigami.NameUtils.initialsFromString("Nate Martin"), "NM") + + compare(Kirigami.NameUtils.isStringUnsuitableForInitials("Kalanoka"), false) + compare(Kirigami.NameUtils.initialsFromString("Kalanoka"), "K") + + compare(Kirigami.NameUtils.isStringUnsuitableForInitials("Why would anyone use such a long not name in the field of the Name"), false) + compare(Kirigami.NameUtils.initialsFromString("Why would anyone use such a long not name in the field of the Name"), "WN") + + compare(Kirigami.NameUtils.isStringUnsuitableForInitials("Live-CD User"), false) + compare(Kirigami.NameUtils.initialsFromString("Live-CD User"), "LU") + } + // these are just randomly sampled names from internet pages in the + // source languages of the name + function test_jp_name() { + compare(Kirigami.NameUtils.isStringUnsuitableForInitials("北里 柴三郎"), false) + compare(Kirigami.NameUtils.initialsFromString("北里 柴三郎"), "北") + + compare(Kirigami.NameUtils.isStringUnsuitableForInitials("小野田 寛郎"), false) + compare(Kirigami.NameUtils.initialsFromString("小野田 寛郎"), "小") + } + function test_cn_name() { + compare(Kirigami.NameUtils.isStringUnsuitableForInitials("蔣經國"), false) + compare(Kirigami.NameUtils.initialsFromString("蔣經國"), "蔣") + } + function test_bad_names() { + compare(Kirigami.NameUtils.isStringUnsuitableForInitials("151231023"), true) + } + } +} diff --git a/autotests/tst_formlayout.qml b/autotests/tst_formlayout.qml new file mode 100644 index 0000000..2d5a52f --- /dev/null +++ b/autotests/tst_formlayout.qml @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2022 Connor Carney + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Window 2.0 +import QtQuick.Layouts 1.3 +import org.kde.kirigami 2.19 as Kirigami +import QtTest 1.0 + +TestCase { + id: testCase + name: "FormLayout" + + 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 = fractionalSizeRoundingComponent.createObject(); + 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 = fractionalSizeRoundingComponent.createObject(); + 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(); + } +} diff --git a/autotests/tst_icon.qml b/autotests/tst_icon.qml new file mode 100644 index 0000000..2de2409 --- /dev/null +++ b/autotests/tst_icon.qml @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtTest 1.0 +import org.kde.kirigami 2.11 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" } } + + 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)) + } +} diff --git a/autotests/tst_keynavigation.qml b/autotests/tst_keynavigation.qml new file mode 100644 index 0000000..6fc539f --- /dev/null +++ b/autotests/tst_keynavigation.qml @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.7 +import QtQuick.Controls 2.0 +import QtQuick.Window 2.1 +import org.kde.kirigami 2.4 as Kirigami +import QtTest 1.0 +import "../tests" + +TestCase { + id: testCase + width: 400 + height: 400 + name: "KeyboardNavigation" + + KeyboardTest { + id: mainWindow + width: 480 + height: 360 + } + + SignalSpy { + id: spyActive + target: mainWindow + signalName: "activeChanged" + } + SignalSpy { + id: spyLastKey + target: mainWindow.pageStack.currentItem + signalName: "lastKeyChanged" + } + + function initTestCase() { + mainWindow.show() + } + + function cleanupTestCase() { + mainWindow.close() + } + + function test_press() { + compare(mainWindow.pageStack.depth, 2) + compare(mainWindow.pageStack.currentIndex, 1) + if (!mainWindow.active) + spyActive.wait(5000) + verify(mainWindow.active) + keyClick("A") + spyLastKey.wait() + compare(mainWindow.pageStack.currentItem.lastKey, "A") + keyClick(Qt.Key_Left, Qt.AltModifier) + compare(mainWindow.pageStack.currentIndex, 0) + compare(mainWindow.pageStack.currentItem.lastKey, "") + keyClick("B") + spyLastKey.wait() + compare(mainWindow.pageStack.currentItem.lastKey, "B") + } +} diff --git a/autotests/tst_listskeynavigation.qml b/autotests/tst_listskeynavigation.qml new file mode 100644 index 0000000..b964505 --- /dev/null +++ b/autotests/tst_listskeynavigation.qml @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.7 +import QtQuick.Controls 2.0 +import QtQuick.Window 2.1 +import org.kde.kirigami 2.4 as Kirigami +import QtTest 1.0 +import "../tests" + +TestCase { + id: testCase + width: 400 + height: 400 + name: "KeyboardListsNavigation" + + KeyboardListTest { + id: mainWindow + width: 480 + height: 360 + } + + SignalSpy { + id: spyActive + target: mainWindow + signalName: "activeChanged" + } + SignalSpy { + id: spyCurrentIndex + target: mainWindow.pageStack.currentItem.flickable + signalName: "currentIndexChanged" + } + + function initTestCase() { + mainWindow.show() + } + + function cleanupTestCase() { + mainWindow.close() + } + + function test_press() { + compare(mainWindow.pageStack.depth, 1) + compare(mainWindow.pageStack.currentIndex, 0) + if (!mainWindow.active) + spyActive.wait(5000) + verify(mainWindow.active) + compare(mainWindow.pageStack.currentItem.flickable.currentIndex, 0) + keyClick(Qt.Key_Down) + spyCurrentIndex.wait() + compare(mainWindow.pageStack.currentItem.flickable.currentIndex, 1) + } +} diff --git a/autotests/tst_mnemonicdata.qml b/autotests/tst_mnemonicdata.qml new file mode 100644 index 0000000..16debe6 --- /dev/null +++ b/autotests/tst_mnemonicdata.qml @@ -0,0 +1,20 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.0 +import QtQuick.Window 2.1 +import org.kde.kirigami 2.4 as Kirigami +import QtTest 1.0 +import "../tests" + +TestCase { + id: testCase + + Kirigami.MnemonicData.enabled: true + Kirigami.MnemonicData.label: "设置(&S)" + + width: 400 + height: 400 + + function test_press() { + compare(Kirigami.MnemonicData.richTextLabel, "设置") + } +} diff --git a/autotests/tst_pagerouter.qml b/autotests/tst_pagerouter.qml new file mode 100644 index 0000000..bc77ef8 --- /dev/null +++ b/autotests/tst_pagerouter.qml @@ -0,0 +1,110 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 as QQC2 +import org.kde.kirigami 2.12 as Kirigami +import QtTest 1.0 + +Kirigami.PageRow { + id: root + TestCase { + name: "PageRouterGeneralTests" + function test_a_init() { + compare(router.currentRoutes().length, 1) + } + function test_b_navigate() { + router.navigateToRoute(["home", "login"]) + compare(router.currentRoutes().length, 2) + } + function test_c_data() { + router.navigateToRoute(["home", {"route": "login", "data": "red"}]) + compare(router.routeActive(["home", {"route": "login", "data": "red"}]), true) + compare(router.routeActive(["home", {"route": "login", "data": "blue"}]), false) + } + function test_d_cache_works() { + router.navigateToRoute(["home", {"route": "login", "data": "red"}, {"route": "login", "data": "blue"}]) + compare(router.currentRoutes().length, 3) + } + function test_e_push() { + router.pushRoute("home") + compare(router.currentRoutes().length, 4) + } + function test_f_pop() { + router.popRoute() + compare(router.currentRoutes().length, 3) + } + function test_g_bring_to_view() { + router.bringToView("home") + compare(root.columnView.currentIndex, 0) + router.bringToView({"route": "login", "data": "red"}) + compare(root.columnView.currentIndex, 1) + router.bringToView({"route": "login", "data": "blue"}) + compare(root.columnView.currentIndex, 2) + } + function test_h_routeactive() { + compare(router.routeActive(["home"]), true) + compare(router.routeActive(["home", "login"]), true) + compare(router.routeActive(["home", {"route": "login", "data": "red"}]), true) + compare(router.routeActive(["home", {"route": "login", "data": "blue"}]), false) + } + function test_i_initial_route() { + router.initialRoute = "login" + compare(router.routeActive(["login"]), false) + compare(router.currentRoutes().length, 3) + } + function test_j_navigation_two() { + router.navigateToRoute(["home", {"route": "login", "data": "red"}, {"route": "login", "data": "blue"}]) + compare(router.currentRoutes().length, 3) + router.navigateToRoute(["home"]) + compare(router.currentRoutes().length, 1) + compare(router.pageStack.count, 1) + } + } + Kirigami.PageRouter { + id: router + initialRoute: "home" + pageStack: root.columnView + + Kirigami.PageRoute { + name: "home" + cache: false + Component { + Kirigami.Page { + Column { + Kirigami.Heading { + text: "Welcome" + } + QQC2.Button { + text: "Red Login" + onClicked: Kirigami.PageRouter.navigateToRoute(["home", {"route": "login", "data": "red"}]) + } + QQC2.Button { + text: "Blue Login" + onClicked: Kirigami.PageRouter.navigateToRoute(["home", {"route": "login", "data": "blue"}]) + } + } + } + } + } + Kirigami.PageRoute { + name: "login" + cache: true + Component { + Kirigami.Page { + Column { + Kirigami.Heading { + text: "Login" + } + Rectangle { + height: 50 + width: 50 + color: Kirigami.PageRouter.data + } + QQC2.Button { + text: "Back to Home" + onClicked: Kirigami.PageRouter.navigateToRoute("home") + } + } + } + } + } + } +} \ No newline at end of file diff --git a/autotests/tst_pagerow.qml b/autotests/tst_pagerow.qml new file mode 100644 index 0000000..b3a972f --- /dev/null +++ b/autotests/tst_pagerow.qml @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.7 +import QtQuick.Controls 2.0 +import QtQuick.Window 2.1 +import org.kde.kirigami 2.4 as Kirigami +import QtTest 1.0 + +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) + mainWindow.pageStack.push(randomPage) + mainWindow.pageStack.push(randomPage) + compare(mainWindow.pageStack.depth, 2) + compare(mainWindow.pageStack.currentIndex, 1) + compare(spyCurrentIndex.count, 3) + 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) + } + + 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, 2) + } +} diff --git a/autotests/tst_routerwindow.qml b/autotests/tst_routerwindow.qml new file mode 100644 index 0000000..e9ee373 --- /dev/null +++ b/autotests/tst_routerwindow.qml @@ -0,0 +1,109 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 as QQC2 +import org.kde.kirigami 2.12 as Kirigami +import QtTest 1.0 + +TestCase { + name: "RouterWindowTests" + property alias router: root.router + when: windowShown + + function test_a_init() { + compare(router.currentRoutes().length, 1) + } + function test_b_navigate() { + router.navigateToRoute(["home", "login"]) + compare(router.currentRoutes().length, 2) + } + function test_c_data() { + router.navigateToRoute(["home", {"route": "login", "data": "red"}]) + compare(router.routeActive(["home", {"route": "login", "data": "red"}]), true) + compare(router.routeActive(["home", {"route": "login", "data": "blue"}]), false) + } + function test_d_cache_works() { + router.navigateToRoute(["home", {"route": "login", "data": "red"}, {"route": "login", "data": "blue"}]) + compare(router.currentRoutes().length, 3) + } + function test_e_push() { + router.pushRoute("home") + compare(router.currentRoutes().length, 4) + } + function test_f_pop() { + router.popRoute() + compare(router.currentRoutes().length, 3) + } + function test_g_bring_to_view() { + router.bringToView("home") + compare(root.pageStack.currentIndex, 0) + router.bringToView({"route": "login", "data": "red"}) + compare(root.pageStack.currentIndex, 1) + router.bringToView({"route": "login", "data": "blue"}) + compare(root.pageStack.currentIndex, 2) + } + function test_h_routeactive() { + compare(router.routeActive(["home"]), true) + compare(router.routeActive(["home", "login"]), true) + compare(router.routeActive(["home", {"route": "login", "data": "red"}]), true) + compare(router.routeActive(["home", {"route": "login", "data": "blue"}]), false) + } + function test_i_initial_route() { + router.initialRoute = "login" + compare(router.routeActive(["login"]), false) + compare(router.currentRoutes().length, 3) + } + function test_j_navigation_two() { + router.navigateToRoute(["home", {"route": "login", "data": "red"}, {"route": "login", "data": "blue"}]) + compare(router.currentRoutes().length, 3) + router.navigateToRoute(["home"]) + compare(router.currentRoutes().length, 1) + compare(router.pageStack.count, 1) + } + + Kirigami.RouterWindow { + id: root + initialRoute: "home" + Kirigami.PageRoute { + name: "home" + cache: false + Component { + Kirigami.Page { + Column { + Kirigami.Heading { + text: "Welcome" + } + QQC2.Button { + text: "Red Login" + onClicked: Kirigami.PageRouter.navigateToRoute(["home", {"route": "login", "data": "red"}]) + } + QQC2.Button { + text: "Blue Login" + onClicked: Kirigami.PageRouter.navigateToRoute(["home", {"route": "login", "data": "blue"}]) + } + } + } + } + } + Kirigami.PageRoute { + name: "login" + cache: true + Component { + Kirigami.Page { + Column { + Kirigami.Heading { + text: "Login" + } + Rectangle { + height: 50 + width: 50 + color: Kirigami.PageRouter.data + } + QQC2.Button { + text: "Back to Home" + onClicked: Kirigami.PageRouter.navigateToRoute("home") + } + } + } + } + } + } +} \ No newline at end of file diff --git a/autotests/tst_theme.qml b/autotests/tst_theme.qml new file mode 100644 index 0000000..ac1c516 --- /dev/null +++ b/autotests/tst_theme.qml @@ -0,0 +1,325 @@ +/* + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtTest 1.0 +import org.kde.kirigami 2.11 as Kirigami + +TestCase { + id: testCase + name: "ThemeTest" + + width: 400 + height: 400 + visible: true + + when: windowShown + + TextMetrics { + id: textMetrics + } + + // Not all properties are updated immediately to avoid having massive storms + // of duplicated signals and to prevent changes from retriggering code that + // changed it. To deal with that, we need to wait a bit before continuiing + // when we change properties. This time shouldn't be too short because on + // some machines it may take a bit longer for things to properly be updated. + function waitForEvents() + { + wait(20) + } + + 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" + + // Changes to Theme are not immediately propagated, so give it a few + // moments. + waitForEvents() + + 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 + + waitForEvents() + + compare(item.color, "#00ff00") + } + + Component { + id: inherit + + Rectangle { + 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 + + waitForEvents() + + 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" + + waitForEvents() + + 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 + + waitForEvents() + + 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" + + waitForEvents() + + compare(item.color, "#0000ff") + compare(item.child1.color, "#00ff00") + compare(item.child2.color, "#eff0f1") + } + + Component { + id: deepInherit + + Rectangle { + 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) + + waitForEvents() + + compare(item.color, "#eff0f1") + compare(item.child1.color, "#eff0f1") + compare(item.child2.color, "#eff0f1") + compare(item.child3.color, "#eff0f1") + + item.Kirigami.Theme.backgroundColor = "#ff0000" + + waitForEvents() + + 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" + + waitForEvents() + + 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 + + waitForEvents() + + 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 + + waitForEvents() + + compare(item.color, "#ff0000") + compare(item.child1.color, "#ff0000") + compare(item.child2.color, "#31363b") + compare(item.child3.color, "#31363b") + } + + Component { + id: colorSet + + Rectangle { + Kirigami.Theme.colorSet: Kirigami.Theme.View + color: Kirigami.Theme.backgroundColor + } + } + + function test_colorset() { + var item = createTemporaryObject(colorSet, testCase) + verify(item) + + waitForEvents() + + compare(item.color, "#fcfcfc") + + item.Kirigami.Theme.colorSet = Kirigami.Theme.Complementary + + waitForEvents() + + compare(item.color, "#31363b") + } + + Component { + id: colorGroup + + Rectangle { + Kirigami.Theme.colorGroup: Kirigami.Theme.Disabled + color: Kirigami.Theme.backgroundColor + } + } + + function test_colorGroup() { + var item = createTemporaryObject(colorGroup, testCase) + verify(item) + + waitForEvents() + + 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 + + waitForEvents() + + 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() { + var item = createTemporaryObject(palette, testCase) + verify(item) + + compare(item.child.background.color, "#eff0f1") + compare(item.child.contentItem.color, "#31363b") + + item.Kirigami.Theme.backgroundColor = "#ff0000" + + waitForEvents() + + compare(item.child.background.color, "#ff0000") + } +} diff --git a/autotests/wheelhandler/ContentFlickable.qml b/autotests/wheelhandler/ContentFlickable.qml new file mode 100644 index 0000000..0b83159 --- /dev/null +++ b/autotests/wheelhandler/ContentFlickable.qml @@ -0,0 +1,64 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 + +Flickable { + id: flickable + property real cellWidth: 60 + property real cellHeight: 60 + readonly property QQC2.Button enableSliderButton: enableSliderButton + readonly property QQC2.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..dec475b --- /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 2.15 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.19 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..eb523ac --- /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 2.15 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.19 as Kirigami +import QtTest 1.15 + +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..e77e1bd --- /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 2.15 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.19 as Kirigami +import QtTest 1.15 + +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() + verify(flickable.contentX === x + hstep, "scrollRight()") + x = flickable.contentX + + wheelHandler.scrollLeft() + verify(flickable.contentX === x - hstep, "scrollLeft()") + x = flickable.contentX + + wheelHandler.scrollDown() + verify(flickable.contentY === y + vstep, "scrollDown()") + y = flickable.contentY + + wheelHandler.scrollUp() + verify(flickable.contentY === y - vstep, "scrollUp()") + y = flickable.contentY + + wheelHandler.scrollRight(101) + verify(flickable.contentX === x + 101, "scrollRight(101)") + x = flickable.contentX + + wheelHandler.scrollLeft(101) + verify(flickable.contentX === x - 101, "scrollLeft(101)") + x = flickable.contentX + + wheelHandler.scrollDown(101) + verify(flickable.contentY === y + 101, "scrollDown(101)") + y = flickable.contentY + + wheelHandler.scrollUp(101) + verify(flickable.contentY === y - 101, "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..818052e --- /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 2.15 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.19 as Kirigami +import QtTest 1.15 + +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..6f6f321 --- /dev/null +++ b/autotests/wheelhandler/tst_scrolling.qml @@ -0,0 +1,180 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.19 as Kirigami +import QtTest 1.15 + +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 = 120) { + let x = flickable.contentX + let y = flickable.contentY + const angleDeltaFactor = angleDelta / 120 + mouseWheel(flickable, flickable.leftMargin, 0, -angleDelta, -angleDelta, Qt.NoButton) + verify(flickable.contentX === Math.round(x + hstep * angleDeltaFactor), "+xTick") + x = flickable.contentX + verify(flickable.contentY === Math.round(y + vstep * angleDeltaFactor), "+yTick") + y = flickable.contentY + + mouseWheel(flickable, flickable.leftMargin, 0, angleDelta, angleDelta, Qt.NoButton) + verify(flickable.contentX === Math.round(x - hstep * angleDeltaFactor), "-xTick") + x = flickable.contentX + verify(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) + verify(flickable.contentX === Math.round(x + hstep * angleDeltaFactor), "+h_yTick") + x = flickable.contentX + verify(flickable.contentY === y, "no +yTick") + + mouseWheel(flickable, flickable.leftMargin, 0, 0, angleDelta, Qt.NoButton, Qt.AltModifier) + verify(flickable.contentX === Math.round(x - hstep * angleDeltaFactor), "-h_yTick") + x = flickable.contentX + verify(flickable.contentY === y, "no -yTick") + } + + mouseWheel(flickable, flickable.leftMargin, 0, -angleDelta, -angleDelta, Qt.NoButton, wheelHandler.pageScrollModifiers) + verify(flickable.contentX === Math.round(x + pageWidth * angleDeltaFactor), "+xPage") + x = flickable.contentX + verify(flickable.contentY === Math.round(y + pageHeight * angleDeltaFactor), "+yPage") + y = flickable.contentY + + mouseWheel(flickable, flickable.leftMargin, 0, angleDelta, angleDelta, Qt.NoButton, wheelHandler.pageScrollModifiers) + verify(flickable.contentX === Math.round(x - pageWidth * angleDeltaFactor), "-xPage") + x = flickable.contentX + verify(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) + verify(flickable.contentX === Math.round(x + pageWidth * angleDeltaFactor), "+h_yPage") + x = flickable.contentX + verify(flickable.contentY === y, "no +yPage") + + mouseWheel(flickable, flickable.leftMargin, 0, 0, angleDelta, Qt.NoButton, + Qt.AltModifier | wheelHandler.pageScrollModifiers) + verify(flickable.contentX === Math.round(x - pageWidth * angleDeltaFactor), "-h_yPage") + x = flickable.contentX + verify(flickable.contentY === y, "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) + verify(flickable.contentX === x + hstep, "Key_Right") + x = flickable.contentX + + keyClick(Qt.Key_Left) + verify(flickable.contentX === x - hstep, "Key_Left") + x = flickable.contentX + + keyClick(Qt.Key_Down) + verify(flickable.contentY === y + vstep, "Key_Down") + y = flickable.contentY + + keyClick(Qt.Key_Up) + verify(flickable.contentY === y - vstep, "Key_Up") + y = flickable.contentY + + keyClick(Qt.Key_PageDown) + verify(flickable.contentY === y + pageHeight, "Key_PageDown") + y = flickable.contentY + + keyClick(Qt.Key_PageUp) + verify(flickable.contentY === y - pageHeight, "Key_PageUp") + y = flickable.contentY + + keyClick(Qt.Key_End) + verify(flickable.contentY === contentHeight - pageHeight, "Key_End") + y = flickable.contentY + + keyClick(Qt.Key_Home) + verify(flickable.contentY === originalY, "Key_Home") + y = flickable.contentY + + keyClick(Qt.Key_PageDown, Qt.AltModifier) + verify(flickable.contentX === x + pageWidth, "h_Key_PageDown") + x = flickable.contentX + + keyClick(Qt.Key_PageUp, Qt.AltModifier) + verify(flickable.contentX === x - pageWidth, "h_Key_PageUp") + x = flickable.contentX + + keyClick(Qt.Key_End, Qt.AltModifier) + verify(flickable.contentX === contentWidth - pageWidth, "h_Key_End") + x = flickable.contentX + + keyClick(Qt.Key_Home, Qt.AltModifier) + verify(flickable.contentX === originalX, "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/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/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/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..217c8b9 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,3 @@ + +add_subdirectory(applicationitemapp) + diff --git a/examples/applicationitemapp/CMakeLists.txt b/examples/applicationitemapp/CMakeLists.txt new file mode 100644 index 0000000..725c34b --- /dev/null +++ b/examples/applicationitemapp/CMakeLists.txt @@ -0,0 +1,13 @@ + +set(applicationitemapp_SRCS + main.cpp + ) + +qt_add_resources(RESOURCES resources.qrc) + +add_executable(applicationitemapp ${applicationitemapp_SRCS} ${RESOURCES}) +target_link_libraries(applicationitemapp Qt5::Core Qt5::Qml Qt5::Quick Qt5::Svg) + +install(TARGETS applicationitemapp ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) + +include(${CMAKE_SOURCE_DIR}/KF5Kirigami2Macros.cmake) diff --git a/examples/applicationitemapp/main.cpp b/examples/applicationitemapp/main.cpp new file mode 100644 index 0000000..5458cf2 --- /dev/null +++ b/examples/applicationitemapp/main.cpp @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include +#include +#include +#include + +Q_DECL_EXPORT int main(int argc, char *argv[]) +{ + QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + + QApplication app(argc, argv); + + 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..faf169b --- /dev/null +++ b/examples/applicationitemapp/main.qml @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import org.kde.kirigami 2.4 as Kirigami + +Kirigami.ApplicationItem { + id: root + + globalDrawer: Kirigami.GlobalDrawer { + title: "Hello App" + 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: "action 3" + }, + Kirigami.Action { + text: "action 4" + } + ] + handleVisible: true + } + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + } + + pageStack.initialPage: mainPageComponent + + Component { + id: mainPageComponent + Kirigami.Page { + title: "Hello" + actions { + contextualActions: [ + Kirigami.Action { + text: "action 1" + }, + Kirigami.Action { + text: "action 2" + } + ] + } + Rectangle { + color: "red" + 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..5bf3ca3 --- /dev/null +++ b/examples/flexcolumn/main.qml @@ -0,0 +1,30 @@ +import QtQuick 2.10 +import QtQuick.Layouts 1.12 +import org.kde.kirigami 2.14 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/hero.qml b/examples/hero.qml new file mode 100644 index 0000000..1edf9f9 --- /dev/null +++ b/examples/hero.qml @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Layouts 1.4 +import org.kde.kirigami 2.15 as Kirigami + +Kirigami.ApplicationWindow { + id: root + + pageStack.initialPage: Kirigami.Page { + Kirigami.Avatar { + id: avvy + name: "Janet Doe" + anchors.centerIn: parent + + MouseArea { + anchors.fill: parent + onClicked: { + let page = root.pageStack.layers.push(layer) + page.hero.source = avvy + page.hero.open() + } + } + } + } + + Component { + id: layer + + Kirigami.Page { + id: page + title: "Oh No" + property Kirigami.Hero hero: Kirigami.Hero { + destination: stackAv + } + + Kirigami.Avatar { + id: stackAv + name: "John Doe" + width: height + height: Kirigami.Units.gridUnit * 20 + anchors.centerIn: parent + + MouseArea { + anchors.fill: parent + onClicked: { + page.hero.close() + root.pageStack.layers.pop() + } + } + } + } + } +} diff --git a/examples/icon/CustomSource.qml b/examples/icon/CustomSource.qml new file mode 100644 index 0000000..4320530 --- /dev/null +++ b/examples/icon/CustomSource.qml @@ -0,0 +1,3 @@ +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..cc6f731 --- /dev/null +++ b/examples/icon/Fallback.qml @@ -0,0 +1,4 @@ +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..afba320 --- /dev/null +++ b/examples/icon/FilesystemSource.qml @@ -0,0 +1,3 @@ +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..44aae73 --- /dev/null +++ b/examples/icon/IconThemeSource.qml @@ -0,0 +1,3 @@ +Kirigami.Icon { + source: "view-refresh" +} \ No newline at end of file diff --git a/examples/icon/InternetSource.qml b/examples/icon/InternetSource.qml new file mode 100644 index 0000000..8fa2f46 --- /dev/null +++ b/examples/icon/InternetSource.qml @@ -0,0 +1,3 @@ +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..1f0ded4 --- /dev/null +++ b/examples/icon/ResourceSource.qml @@ -0,0 +1,3 @@ +Kirigami.Icon { + source: "qrc:/kirigami.svg" +} diff --git a/examples/imagecolorstest.qml b/examples/imagecolorstest.qml new file mode 100644 index 0000000..ae42ba4 --- /dev/null +++ b/examples/imagecolorstest.qml @@ -0,0 +1,152 @@ + +import QtQuick 2.12 +import QtQuick.Layouts 1.4 +import QtQuick.Controls 2.12 as Controls +import org.kde.kirigami 2.13 as Kirigami + +RowLayout { + id: root + width: 500 + height: 500 + + property var 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 i + + Kirigami.ImageColors { + id: palette + source: icon.source + } + Kirigami.ImageColors { + id: imgPalette + source: image + } + + ColumnLayout { + Rectangle { + Layout.preferredWidth: 200 + Layout.preferredHeight: 200 + z: -1 + color: palette.dominantContrast + Kirigami.Icon { + id: icon + anchors.centerIn: parent + width: 128 + height: 128 + source: "desktop" + } + } + Rectangle { + Layout.preferredWidth: 30 + Layout.preferredHeight: 30 + color: palette.average + } + Controls.Button { + text: "Next" + onClicked: { + i = (i+1)%icons.length + icon.source = icons[i] + // palette.update() + } + } + + Repeater { + model: palette.palette + delegate: RowLayout { + Layout.fillWidth: true + Rectangle { + implicitWidth: 10 + 300 * modelData.ratio + implicitHeight: 30 + color: modelData.color + } + Item { + Layout.fillWidth: true + } + Rectangle { + color: modelData.contrastColor + implicitWidth: 30 + implicitHeight: 30 + } + } + } + } + Item { + Layout.preferredWidth: 500 + Layout.preferredHeight: 500/(image.sourceSize.width/image.sourceSize.height) + Image { + id: image + source: "https://source.unsplash.com/random" + anchors.fill: parent + onStatusChanged: imgPalette.update() + } + ColumnLayout { + Controls.Button { + text: "Update" + onClicked: { + image.source = "https://source.unsplash.com/random#" + (new Date()).getMilliseconds() + } + } + Repeater { + model: imgPalette.palette + delegate: RowLayout { + Layout.fillWidth: true + Rectangle { + implicitWidth: 10 + 300 * modelData.ratio + implicitHeight: 30 + color: modelData.color + } + Item { + Layout.fillWidth: true + } + Rectangle { + color: modelData.contrastColor + implicitWidth: 30 + implicitHeight: 30 + } + } + } + } + Item { + width: 300 + height: 150 + Kirigami.Theme.backgroundColor: imgPalette.background + Kirigami.Theme.textColor: imgPalette.foreground + Kirigami.Theme.highlightColor: imgPalette.highlight + + anchors { + bottom: parent.bottom + right: parent.right + } + + Rectangle { + anchors.fill: parent + opacity: 0.8 + color: Kirigami.Theme.backgroundColor + } + ColumnLayout { + anchors.centerIn: parent + RowLayout { + Rectangle { + Layout.alignment: Qt.AlignCenter + implicitWidth: 10 + implicitHeight: 10 + color: Kirigami.Theme.highlightColor + } + Controls.Label { + text: "Lorem Ipsum dolor sit amet" + color: Kirigami.Theme.textColor + } + } + RowLayout { + Controls.TextField { + Kirigami.Theme.inherit: true + text: "text" + } + Controls.Button { + Kirigami.Theme.inherit: true + text: "Ok" + } + } + } + } + } +} diff --git a/examples/multiplatformnotesapp/NotesGeneral.qml b/examples/multiplatformnotesapp/NotesGeneral.qml new file mode 100644 index 0000000..5485010 --- /dev/null +++ b/examples/multiplatformnotesapp/NotesGeneral.qml @@ -0,0 +1,200 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.0 as QQC2 +import org.kde.kirigami 2.4 as Kirigami + +Kirigami.ApplicationWindow { + id: root + property string currentFile + + + pageStack.initialPage: iconView + + Kirigami.ScrollablePage { + id: iconView + title: "Notes" + actions { + contextualActions: [ + Kirigami.Action { + id: sortAction + icon.name: "view-sort-ascending-symbolic" + tooltip: "Sort Ascending" + } + ] + } + background: Rectangle { + color: Kirigami.Thmeme.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 { + 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 " + modelData + } + } + onClicked: { + view.currentIndex = index; + root.currentFile = "File " + 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 { + main: Kirigami.Action { + id: shareAction + icon.name: "document-share" + text: "Share..." + tooltip: "Share this document with your device" + checkable: true + onCheckedChanged: sheet.sheetOpen = checked; + } + contextualActions: [ + 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.Theme.backgroundColor + Rectangle { + anchors.fill: parent + color: "yellow" + opacity: 0.2 + } + } + + Kirigami.OverlaySheet { + id: sheet + onSheetOpenChanged: shareAction.checked = sheetOpen + ListView { + implicitWidth: Kirigami.Units.gridUnit * 30 + 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: Kirigami.AbstractListItem { + height: Kirigami.Units.gridUnit * 6 + hoverEnabled: false + RowLayout { + Kirigami.Icon { + source: "documentinfo" + width: Kirigami.Units.iconSizes.large + height: width + } + QQC2.Label { + Layout.fillWidth: true + Layout.minimumWidth: 0 + wrapMode: Text.WordWrap + 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" + } + } + } + delegate: Kirigami.AbstractListItem { + height: Kirigami.Units.gridUnit * 6 + hoverEnabled: false + //TODO: bug in overlaysheet + rightPadding: Kirigami.Units.gridUnit * 1.5 + RowLayout { + ColumnLayout { + Layout.fillWidth: true + Layout.minimumWidth: 0 + QQC2.Label { + wrapMode: Text.WordWrap + text: model.title + } + QQC2.Label { + Layout.fillWidth: true + Layout.minimumWidth: 0 + wrapMode: Text.WordWrap + text: model.description + } + } + QQC2.Button { + text: model.buttonText + onClicked: sheet.close() + } + } + } + } + } + QQC2.TextArea { + background: Item {} + wrapMode: TextEdit.WordWrap + 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..fb85732 --- /dev/null +++ b/examples/multiplatformnotesapp/notesDesktop.qml @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.0 as QQC2 +import org.kde.kirigami 2.4 as Kirigami + +NotesGeneral { + id: root +} diff --git a/examples/multiplatformnotesapp/notesMobile.qml b/examples/multiplatformnotesapp/notesMobile.qml new file mode 100644 index 0000000..553b8e9 --- /dev/null +++ b/examples/multiplatformnotesapp/notesMobile.qml @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.0 as QQC2 +import org.kde.kirigami 2.4 as Kirigami + +NotesGeneral { + id: root + + contextDrawer: Kirigami.ContextDrawer {} +} diff --git a/examples/pagerouter/PageRoute.qml b/examples/pagerouter/PageRoute.qml new file mode 100644 index 0000000..6287f31 --- /dev/null +++ b/examples/pagerouter/PageRoute.qml @@ -0,0 +1,8 @@ +Kirigami.PageRoute { + name: "routeOne" + Component { + Kirigami.Page { + // Page contents... + } + } +} \ No newline at end of file diff --git a/examples/pagerouter/PageRouter.qml b/examples/pagerouter/PageRouter.qml new file mode 100644 index 0000000..433e544 --- /dev/null +++ b/examples/pagerouter/PageRouter.qml @@ -0,0 +1,56 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 as QQC2 +import org.kde.kirigami 2.12 as Kirigami + +Kirigami.ApplicationWindow { + id: root + + Kirigami.PageRouter { + initialRoute: "home" + pageStack: root.pageStack.columnView + + Kirigami.PageRoute { + name: "home" + cache: false + Component { + Kirigami.Page { + Column { + Kirigami.Heading { + text: "Welcome" + } + QQC2.Button { + text: "Red Login" + onClicked: Kirigami.PageRouter.navigateToRoute(["home", {"route": "login", "data": "red"}]) + } + QQC2.Button { + text: "Blue Login" + onClicked: Kirigami.PageRouter.navigateToRoute(["home", {"route": "login", "data": "blue"}]) + } + } + } + } + } + Kirigami.PageRoute { + name: "login" + cache: true + Component { + Kirigami.Page { + Column { + Kirigami.Heading { + text: "Login" + } + Rectangle { + height: 50 + width: 50 + color: Kirigami.PageRouter.data + } + QQC2.Button { + text: "Back to Home" + onClicked: Kirigami.PageRouter.navigateToRoute("home") + } + } + } + } + } + } +} diff --git a/examples/pagerouter/PageRouterCachePagesDo.qml b/examples/pagerouter/PageRouterCachePagesDo.qml new file mode 100644 index 0000000..a1166e4 --- /dev/null +++ b/examples/pagerouter/PageRouterCachePagesDo.qml @@ -0,0 +1,23 @@ +import QtQuick 2.12 +import org.kde.kirigami 2.12 as Kirigami + +Kirigami.ApplicationWindow { + id: root + Kirigami.PageRouter { + initialRoute: "home" + pageStack: root.pageStack.columnView + + // home can be pushed onto the route twice + Component.onCompleted: navigateToRoute("home", "home") + + Kirigami.PageRoute { + name: "home" + cache: false + Component { + Kirigami.Page { + // Page contents... + } + } + } + } +} \ No newline at end of file diff --git a/examples/pagerouter/PageRouterCachePagesDont.qml b/examples/pagerouter/PageRouterCachePagesDont.qml new file mode 100644 index 0000000..afa7d03 --- /dev/null +++ b/examples/pagerouter/PageRouterCachePagesDont.qml @@ -0,0 +1,23 @@ +import QtQuick 2.12 +import org.kde.kirigami 2.12 as Kirigami + +Kirigami.ApplicationWindow { + id: root + Kirigami.PageRouter { + initialRoute: "home" + pageStack: root.pageStack.columnView + + // home can't be pushed onto the route twice + Component.onCompleted: navigateToRoute("home", "home") + + Kirigami.PageRoute { + name: "home" + cache: true + Component { + Kirigami.Page { + // Page contents... + } + } + } + } +} \ No newline at end of file diff --git a/examples/pagerouter/PageRouterColumnView.qml b/examples/pagerouter/PageRouterColumnView.qml new file mode 100644 index 0000000..428919b --- /dev/null +++ b/examples/pagerouter/PageRouterColumnView.qml @@ -0,0 +1,20 @@ +import QtQuick 2.12 +import org.kde.kirigami 2.12 as Kirigami + +Kirigami.ApplicationWindow { + id: root + Kirigami.PageRouter { + pageStack: root.pageStack.columnView + + Kirigami.PageRoute { + name: "home" + Component { + Kirigami.Page { + // Page contents... + } + } + } + initialRoute: "home" + Component.onCompleted: navigateToRoute("home", "home") + } +} \ No newline at end of file diff --git a/examples/pagerouter/PageRouterInitialRoute.qml b/examples/pagerouter/PageRouterInitialRoute.qml new file mode 100644 index 0000000..1e11b3d --- /dev/null +++ b/examples/pagerouter/PageRouterInitialRoute.qml @@ -0,0 +1,27 @@ +import QtQuick 2.12 +import org.kde.kirigami 2.12 as Kirigami + +Kirigami.ApplicationWindow { + id: root + Kirigami.PageRouter { + initialRoute: "home" + pageStack: root.pageStack.columnView + + Kirigami.PageRoute { + name: "home" + Component { + Kirigami.Page { + // This page will show up when starting the application + } + } + } + Kirigami.PageRoute { + name: "login" + Component { + Kirigami.Page { + // Page contents... + } + } + } + } +} \ No newline at end of file diff --git a/examples/pagerouter/PageRouterRoutes.qml b/examples/pagerouter/PageRouterRoutes.qml new file mode 100644 index 0000000..86ea449 --- /dev/null +++ b/examples/pagerouter/PageRouterRoutes.qml @@ -0,0 +1,34 @@ +import QtQuick 2.12 +import org.kde.kirigami 2.12 as Kirigami + +Kirigami.ApplicationWindow { + id: root + Kirigami.PageRouter { + pageStack: root.pageStack.columnView + + Kirigami.PageRoute { + name: "routeOne" + Component { + Kirigami.Page { + // Page contents... + } + } + } + Kirigami.PageRoute { + name: "routeTwo" + Component { + Kirigami.Page { + // Page contents... + } + } + } + Kirigami.PageRoute { + name: "routeThree" + Component { + Kirigami.Page { + // Page contents... + } + } + } + } +} \ No newline at end of file diff --git a/examples/pagerouter/PageRouterWatchedRoute.qml b/examples/pagerouter/PageRouterWatchedRoute.qml new file mode 100644 index 0000000..6fd8651 --- /dev/null +++ b/examples/pagerouter/PageRouterWatchedRoute.qml @@ -0,0 +1,32 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 as QQC2 +import org.kde.kirigami 2.12 as Kirigami + +Kirigami.ApplicationWindow { + id: applicaionWindow + Kirigami.PageRouter { + initialRoute: "one" + pageStack: applicaionWindow.pageStack.columnView + Kirigami.PageRoute { + name: "one" + Kirigami.Page { + Column { + Kirigami.Heading { + Kirigami.PageRouter.watchedRoute: ["one", "two"] + text: Kirigami.PageRouter.watchedRouteActive ? "/one/two is active" : "only /one is active" + } + QQC2.Button { + text: "Push Two" + onClicked: Kirigami.PageRouter.navigateToRoute(["one", "two"]) + } + } + } + } + Kirigami.PageRoute { + name: "two" + Kirigami.Page { + // Page contents... + } + } + } +} diff --git a/examples/settingscomponents/GeneralSettingsPage.qml b/examples/settingscomponents/GeneralSettingsPage.qml new file mode 100644 index 0000000..739ac3a --- /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 2.15 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.11 as Kirigami +import QtQuick.Layouts 1.15 + +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..e176049 --- /dev/null +++ b/examples/settingscomponents/SettingsPage.qml @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2021 Felipe Kinoshita + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.18 as Kirigami +import QtQuick.Layouts 1.15 + +Kirigami.CategorizedSettings { + actions: [ + Kirigami.SettingAction { + text: 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..4cd9108 --- /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 2.15 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.11 as Kirigami +import QtQuick.Layouts 1.15 + +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..40a4b72 --- /dev/null +++ b/examples/simpleexamples/AbstractApplicationWindow.qml @@ -0,0 +1,151 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.0 as Controls +import org.kde.kirigami 2.4 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 ? pageStack.currentItem.objectName == "settingsPage" : false + onCurrentChanged: { + checked = current; + } + onTriggered: { + pageStack.push(settingsComponent); + } + } + ] + + Controls.CheckBox { + checked: true + text: "Option 1" + } + Controls.CheckBox { + text: "Option 2" + } + Controls.CheckBox { + text: "Option 3" + } + Controls.Slider { + Layout.fillWidth: true + value: 0.5 + } + } + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + } + + pageStack: Controls.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: { + 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..a82941e --- /dev/null +++ b/examples/simpleexamples/FixedSidebar.qml @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.4 as Kirigami +import QtQuick.Controls 2.0 as Controls + +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 + + Controls.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..431d46a --- /dev/null +++ b/examples/simpleexamples/MultipleColumnsGallery.qml @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.0 +import QtQuick.Controls 2.0 as Controls +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.8 + +ScrollablePage { + id: page + Layout.fillWidth: true + implicitWidth: Units.gridUnit * (Math.floor(Math.random() * 35) + 8) + + title: "Multiple Columns" + + actions { + contextualActions: [ + Action { + text:"Action for buttons" + icon.name: "bookmarks" + onTriggered: print("Action 1 clicked") + }, + Action { + text:"Action 2" + icon.name: "folder" + enabled: false + } + ] + } + + ColumnLayout { + width: page.width + spacing: Units.smallSpacing + + Controls.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: Units.gridUnit *2 + Layout.minimumHeight: Layout.minimumWidth + } + Controls.Label { + Layout.alignment: Qt.AlignHCenter + text: "Page implicitWidth: " + page.implicitWidth + } + Controls.Button { + text: "Push Another Page" + Layout.alignment: Qt.AlignHCenter + onClicked: pageStack.push(Qt.resolvedUrl("MultipleColumnsGallery.qml")); + } + Controls.Button { + text: "Pop A Page" + Layout.alignment: Qt.AlignHCenter + onClicked: pageStack.pop(); + } + RowLayout { + Layout.alignment: Qt.AlignHCenter + Controls.TextField { + id: edit + text: page.title + } + Controls.Button { + text: "Rename Page" + onClicked: page.title = edit.text; + } + } + SearchField { + Layout.alignment: Qt.AlignHCenter + id: searchField + onAccepted: console.log("Search text is " + searchField.text); + } + PasswordField { + Layout.alignment: Qt.AlignHCenter + id: passwordField + onAccepted: console.log("Password") + } + } +} diff --git a/examples/simpleexamples/Sidebar.qml b/examples/simpleexamples/Sidebar.qml new file mode 100644 index 0000000..471d316 --- /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 2.1 +import QtQuick.Controls 2.3 as Controls +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.7 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 + + Controls.Label { + Layout.alignment: Qt.AlignHCenter + text: "This is a sidebar" + Layout.fillWidth: true + width: parent.width - Kirigami.Units.smallSpacing * 2 + wrapMode: Text.WordWrap + } + Controls.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 + + Controls.Label { + Layout.alignment: Qt.AlignHCenter + text: "This is a sidebar" + Layout.fillWidth: true + width: parent.width - Kirigami.Units.smallSpacing * 2 + wrapMode: Text.WordWrap + } + Controls.Button { + Layout.alignment: Qt.AlignHCenter + text: "Modal" + checkable: true + Layout.fillWidth: true + checked: false + onCheckedChanged: contextDrawer.modal = checked + } + Item { + Layout.fillHeight: true + } + } + } + + menuBar: Controls.MenuBar { + Controls.Menu { + title: qsTr("&File") + Controls.Action { text: qsTr("&New...") } + Controls.Action { text: qsTr("&Open...") } + Controls.Action { text: qsTr("&Save") } + Controls.Action { text: qsTr("Save &As...") } + Controls.MenuSeparator { } + Controls.Action { text: qsTr("&Quit") } + } + Controls.Menu { + title: qsTr("&Edit") + Controls.Action { text: qsTr("Cu&t") } + Controls.Action { text: qsTr("&Copy") } + Controls.Action { text: qsTr("&Paste") } + } + Controls.Menu { + title: qsTr("&Help") + Controls.Action { text: qsTr("&About") } + } + } + header: Controls.ToolBar { + contentItem: RowLayout { + Controls.ToolButton { + text: "Global ToolBar" + } + Item { + Layout.fillWidth: true + } + Kirigami.ActionTextField { + id: searchField + + placeholderText: "Search..." + + focusSequence: "Ctrl+F" + leftActions: [ + Kirigami.Action { + icon.name: "edit-clear" + visible: searchField.text != "" + onTriggered: { + searchField.text = "" + searchField.accepted() + } + }, + Kirigami.Action { + icon.name: "edit-clear" + visible: searchField.text != "" + onTriggered: { + searchField.text = "" + searchField.accepted() + } + } + ] + rightActions: [ + Kirigami.Action { + icon.name: "edit-clear" + visible: searchField.text != "" + onTriggered: { + searchField.text = "" + searchField.accepted() + } + }, + Kirigami.Action { + icon.name: "anchor" + visible: searchField.text != "" + onTriggered: { + searchField.text = "" + searchField.accepted() + } + } + ] + + onAccepted: console.log("Search text is " + searchField.text) + } + } + } + //Main app content + Component { + id: mainPageComponent + MultipleColumnsGallery {} + } + footer: Controls.ToolBar { + position: Controls.ToolBar.Footer + Controls.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..e208936 --- /dev/null +++ b/examples/simpleexamples/SimplePage.qml @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.0 +import QtQuick.Controls 2.0 as Controls +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.8 + +ScrollablePage { + id: page + Layout.fillWidth: true + implicitWidth: Units.gridUnit * (Math.floor(Math.random() * 35) + 8) + + title: i18n("Simple Scrollable Page") + + actions { + contextualActions: [ + Action { + text:"Action for buttons" + icon.name: "bookmarks" + onTriggered: print("Action 1 clicked") + }, + Action { + text:"Action 2" + icon.name: "folder" + enabled: false + } + ] + } + + ColumnLayout { + width: page.width + spacing: Units.smallSpacing + + Controls.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/TabBarHeader.qml b/examples/simpleexamples/TabBarHeader.qml new file mode 100644 index 0000000..ab1bac0 --- /dev/null +++ b/examples/simpleexamples/TabBarHeader.qml @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.4 as Kirigami + +Kirigami.ApplicationWindow { + id: root + width: 500 + height: 800 + visible: true + + + pageStack.initialPage: mainPageComponent + pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.TabBar + + + Component.onCompleted: { + pageStack.push(mainPageComponent); + pageStack.push(mainPageComponent); + pageStack.currentIndex = 0; + } + + //Main app content + Component { + id: mainPageComponent + MultipleColumnsGallery {} + } + +} diff --git a/examples/simpleexamples/customdrawer.qml b/examples/simpleexamples/customdrawer.qml new file mode 100644 index 0000000..632a7e4 --- /dev/null +++ b/examples/simpleexamples/customdrawer.qml @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import org.kde.kirigami 2.4 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..3df192c --- /dev/null +++ b/examples/simpleexamples/dragPageWidth.qml @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2017 Eike Hein + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 + +import org.kde.kirigami 2.4 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: _lastX = mouseX + + onPositionChanged: { + 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..8c66afc --- /dev/null +++ b/examples/simpleexamples/footer.qml @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.0 as Controls +import org.kde.kirigami 2.4 as Kirigami + +Kirigami.ApplicationWindow { + id: root + + footer: Controls.ToolBar { + //height: Kirigami.Units.gridUnit * 3 + RowLayout { + Controls.ToolButton { + text: "text" + } + } + } + globalDrawer: Kirigami.GlobalDrawer { + title: "Hello App" + 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: "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/minimal.qml b/examples/simpleexamples/minimal.qml new file mode 100644 index 0000000..359c3c5 --- /dev/null +++ b/examples/simpleexamples/minimal.qml @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import org.kde.kirigami 2.4 as Kirigami + +Kirigami.ApplicationWindow { + id: root + + globalDrawer: Kirigami.GlobalDrawer { + title: "Hello App" + 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: "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..7eefbce --- /dev/null +++ b/examples/simpleexamples/pagePoolDrawer.qml @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import org.kde.kirigami 2.11 as Kirigami + +Kirigami.ApplicationWindow { + id: root + + Kirigami.PagePool { + id: mainPagePool + } + + globalDrawer: Kirigami.GlobalDrawer { + title: "Hello App" + titleIcon: "applications-graphics" + 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..858bd7e --- /dev/null +++ b/examples/simpleexamples/pagePoolFirstColumn.qml @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.11 +import org.kde.kirigami 2.11 as Kirigami + +Kirigami.ApplicationWindow { + id: root + + Kirigami.PagePool { + id: mainPagePool + } + + globalDrawer: Kirigami.GlobalDrawer { + title: "Hello App" + titleIcon: "applications-graphics" + } + 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 + delegate: Kirigami.BasicListItem { + id: delegate + action: modelData + } + } + } + } +} diff --git a/examples/simpleexamples/pushpopclear.qml b/examples/simpleexamples/pushpopclear.qml new file mode 100644 index 0000000..ae7a9e6 --- /dev/null +++ b/examples/simpleexamples/pushpopclear.qml @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import org.kde.kirigami 2.4 as Kirigami + +Kirigami.ApplicationWindow { + id: root + + globalDrawer: Kirigami.GlobalDrawer { + title: "Hello App" + titleIcon: "applications-graphics" + + 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 + Kirigami.Label { + text: "First Page" + } + } + } + } + + Component { + id: secondPageComponent + Kirigami.Page { + title: "Second Page" + Rectangle { + color: "red" + anchors.fill: parent + Kirigami.Label { + text: "Second Page" + } + } + } + } + +} diff --git a/examples/simpleexamples/simpleChatApp.qml b/examples/simpleexamples/simpleChatApp.qml new file mode 100644 index 0000000..086594a --- /dev/null +++ b/examples/simpleexamples/simpleChatApp.qml @@ -0,0 +1,324 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.6 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 as QQC2 +import org.kde.kirigami 2.11 as Kirigami + +Kirigami.ApplicationWindow { + id: root + + //header: Kirigami.ToolBarApplicationHeader {} + //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 + title: "Chat App" + titleIcon: "konversation" + 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: applicationWindow == undefined ? false : applicationWindow().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.pointSize: Kirigami.Units.fontMetrics.font.pointSize * 0.8 + 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 { + model: 50 + delegate: Kirigami.BasicListItem { + label: "Person " + modelData + separatorVisible: false + reserveSpaceForIcon: false + } + } + } + + Kirigami.Separator { + Layout.fillWidth: true + Layout.maximumHeight: 1//implicitHeight + } + Kirigami.BasicListItem { + label: "Group call" + icon: "call-start" + separatorVisible: false + } + Kirigami.BasicListItem { + label: "Send Attachment" + icon: "mail-attachment" + separatorVisible: false + } + } + } + + pageStack.defaultColumnWidth: columnWidth + pageStack.initialPage: [channelsComponent, chatComponent] + + Component { + id: channelsComponent + Kirigami.ScrollablePage { + title: "Channels" + actions.main: 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 + //NOTE: icon support in tool button in Qt 5.11 + QQC2.ToolButton { + Layout.fillHeight: true + //make it square + implicitWidth: height + Kirigami.Icon { + anchors.centerIn: parent + width: Kirigami.Units.iconSizes.smallMedium + height: width + source: "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 + delegate: Kirigami.BasicListItem { + label: "#Channel " + modelData + checkable: true + checked: channelsList.currentIndex == index + separatorVisible: false + reserveSpaceForIcon: false + } + } + } + } + + Component { + id: chatComponent + Kirigami.ScrollablePage { + title: "#KDE" + actions { + left: Kirigami.Action { + icon.name: "documentinfo" + text: "Channel info" + } + main: Kirigami.Action { + icon.name: "search" + text: "Search" + } + } + actions.contextualActions: [ + 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 {} + } + //NOTE: icon support in tool button in Qt 5.11 + QQC2.ToolButton { + Layout.fillHeight: true + //make it square + implicitWidth: height + Kirigami.Icon { + anchors.centerIn: parent + width: Kirigami.Units.iconSizes.smallMedium + height: width + source: "go-next" + } + } + } + } + + ListView { + id: channelsList + verticalLayoutDirection: ListView.BottomToTop + currentIndex: 2 + model: 30 + 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 + Kirigami.Icon { + anchors.centerIn: parent + width: Kirigami.Units.iconSizes.smallMedium + height: width + source: "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..bf43173 --- /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(Qt5 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..78f2df3 --- /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/KF5Kirigami2Macros.cmake) + +set(minimal_SRCS + main.cpp + ) + +qt_add_resources(RESOURCES kirigami-icons.qrc resources.qrc) + +if (ANDROID) + set(minimal_EXTRA_LIBS Qt5::AndroidExtras + #FIXME: we shouldn't have to link to it but otherwise the lib won't be packaged on Android + Qt5::QuickControls2) +else () +#qstyle-based qqc2 style needs a QApplication + set(minimal_EXTRA_LIBS Qt5::Widgets) +endif() + + +add_executable(minimal ${minimal_SRCS} ${RESOURCES}) +#kirigamiplugin is the static library built by us +target_link_libraries(minimal kirigamiplugin Qt5::Core Qt5::Qml Qt5::Quick Qt5::QuickControls2 ${minimal_EXTRA_LIBS}) + +#install(TARGETS minimal ${KF5_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..3139673 --- /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 2.7 + +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..9b98a32 --- /dev/null +++ b/examples/staticcmake/src/Page1Form.ui.qml @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.7 +import QtQuick.Controls 2.0 +import QtQuick.Layouts 1.3 +import org.kde.kirigami 2.4 as Kirigami + +Kirigami.Page { + title: qsTr("Page 1") + property alias textField1: textField1 + property alias button1: button1 + actions { + main: 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..5a1e1ce --- /dev/null +++ b/examples/staticcmake/src/main.cpp @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifdef Q_OS_ANDROID +#include +#else +#include +#endif + +#include +#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[]) +{ + QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); +// 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..e6b5607 --- /dev/null +++ b/examples/staticcmake/src/main.qml @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.7 +import QtQuick.Controls 2.0 +import QtQuick.Layouts 1.3 + +import org.kde.kirigami 2.4 as Kirigami + +Kirigami.ApplicationWindow { + visible: true + title: qsTr("Hello World") + + pageStack.initialPage: Page1 {} + + globalDrawer: Kirigami.GlobalDrawer { + title: "Hello App" + 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: "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/swipenavigator/main.qml b/examples/swipenavigator/main.qml new file mode 100644 index 0000000..6e5da98 --- /dev/null +++ b/examples/swipenavigator/main.qml @@ -0,0 +1,34 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.2 +import org.kde.kirigami 2.13 as Kirigami + +Kirigami.ApplicationWindow { + Kirigami.SwipeNavigator { + anchors.fill: parent + initialIndex: 2 + header: Button { + text: "Header" + } + footer: Button { + text: "Footer" + } + Kirigami.Page { + icon.name: "globe" + title: "World Clocks" + } + Kirigami.Page { + icon.name: "clock" + title: "Alarms" + needsAttention: true + } + Kirigami.Page { + icon.name: "clock" + title: "Stopwatch" + } + Kirigami.Page { + icon.name: "clock" + title: "Timers" + progress: 0.5 + } + } +} diff --git a/examples/wheelhandler/FlickableUsage.qml b/examples/wheelhandler/FlickableUsage.qml new file mode 100644 index 0000000..7182fd5 --- /dev/null +++ b/examples/wheelhandler/FlickableUsage.qml @@ -0,0 +1,67 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.19 as Kirigami + +QQC2.ApplicationWindow { + id: root + width: flickable.implicitWidth + height: flickable.implicitHeight + 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..eda0765 --- /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 2.15 +import QtQuick.Templates 2.15 as T +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.19 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.vertical && control.T.ScrollBar.vertical.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.horizontal && control.T.ScrollBar.horizontal.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..9d1b66c --- /dev/null +++ b/metainfo.yaml @@ -0,0 +1,24 @@ +maintainer: mart +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 + note: maintainer needed + - name: Windows + - name: macOS + note: maintainer needed +public_lib: true +deprecated: false +release: true +logo: logo.png +libraries: + - cmake: KF5::Kirigami2 +cmakename: KF5Kirigami2 +irc: kde-kirigami +mailinglist: plasma-devel +group: Frameworks +subgroup: Tier 1 diff --git a/po/ar/libkirigami2plugin_qt.po b/po/ar/libkirigami2plugin_qt.po new file mode 100644 index 0000000..ba38c62 --- /dev/null +++ b/po/ar/libkirigami2plugin_qt.po @@ -0,0 +1,246 @@ +# Safa Alfulaij , 2017, 2018. +# Zayed Al-Saidi , 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-08-08 23:02+0400\n" +"Last-Translator: Zayed Al-Saidi \n" +"Language-Team: Arabic \n" +"Language: ar\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=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 21.07.70\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "‏%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "زر صفحة %1 في متجر كدي" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "أرسِل بريد الإلكتروني إلى %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "شاركنا" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "حقوق النسخ" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "الرّخصة:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "الترخيص: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "المكتبات المستخدمة" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "المؤلفين" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "أظهر صور المطور" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "إشادات" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "المترجمون" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "حول %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "أبلغ عن علّة…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "أنهِ" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "إجراءات أخرى" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "أزل الوسم" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "إجراءات" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "عُد" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "أغلق الشريط الجانبي" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "افتح الشريط الجانبي" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "يحمّل..." + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "كلمة السّرّ" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "ابحث..." + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "ابحث" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "إعدادات" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "إعدادات — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "الصفحة الحالية. التقدم: %1 بالمائة" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "انتقل إلى %1. التقدم: %2 بالمائة" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "الصّفحة الحاليّة" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "انتقل إلى %1. تطلب اهتمام." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "تنقّل إلى %1." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "أغلق الخزنة" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "افتح الخزنة" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "تنقّل للخلف" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "تنقّل للأمام" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "إجراءات أخرى" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "انسخ الوصلة إلى الحافظة" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "إصدارة إطار كدي %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "نظام نوافذ %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "كيوت %2 (بني على %3)" diff --git a/po/ast/libkirigami2plugin_qt.po b/po/ast/libkirigami2plugin_qt.po new file mode 100644 index 0000000..a5643e7 --- /dev/null +++ b/po/ast/libkirigami2plugin_qt.po @@ -0,0 +1,279 @@ +# enolp , 2020, 2021. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2021-04-20 02:13+0200\n" +"Last-Translator: enolp \n" +"Language-Team: Asturian \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 21.03.90\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "" + +#: controls/AboutItem.qml:139 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "Send an email to %1" +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Unviar un corréu a %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:198 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "License:" +msgctxt "AboutItem|" +msgid "License:" +msgstr "Llicencia:" + +#: controls/AboutItem.qml:269 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "License: %1" +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Llicencia: %1" + +#: controls/AboutItem.qml:280 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Libraries in use" +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Biblioteques n'usu" + +#: controls/AboutItem.qml:310 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Authors" +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autores" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:350 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Credits" +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Creitos" + +#: controls/AboutItem.qml:363 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Translators" +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Traductores" + +#: controls/AboutPage.qml:85 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "About" +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Tocante a" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Colar" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Más aiciones" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Aiciones" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Atrás" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Zarrar la barra llateral" + +#: controls/GlobalDrawer.qml:594 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Zarrar la barra llateral" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Contraseña" + +#: controls/SearchField.qml:95 +#, fuzzy +#| msgctxt "SearchField|" +#| msgid "Search" +msgctxt "SearchField|" +msgid "Search…" +msgstr "Buscar…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Buscar…" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, fuzzy, qt-format +#| msgctxt "ForwardButton|" +#| msgid "Navigate Forward" +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Navegar p'alantre" + +#: controls/templates/OverlayDrawer.qml:140 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Zarrar la barra llateral" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navegar p'atrás" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navegar p'alantre" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Más aiciones" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "El sistema de ventanes %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (compilóse con %3)" diff --git a/po/az/libkirigami2plugin_qt.po b/po/az/libkirigami2plugin_qt.po new file mode 100644 index 0000000..b7b9eeb --- /dev/null +++ b/po/az/libkirigami2plugin_qt.po @@ -0,0 +1,244 @@ +# 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:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "%1 KDE Mağazası səhifəsini ziyarət edin" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "%1 ünvanına e-poçt göndərin" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Iştirak edin" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Müəllif hüquqları" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Lisenziya:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Lisenziya: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "İstifadə olunan kitabxana" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Müəlliflər" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Müəllifin fotoşəklləri" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Minnətdarlıq" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Tərcüməçilər" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1 haqqında" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Xəta hesabatı göndərin..." + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Çıxış" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Daha Çox Fəaliyyətlər" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Yarlığı silin" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Əməllər" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Geriyə" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Yandakı paneli bağla" + +#: controls/GlobalDrawer.qml:594 +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:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Şifrə" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Axtarış" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Axtarış" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Ayarlar" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Ayarlar — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Cari səhifə. Gedişat: %1 faiz." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "İstiqamət %1. Gedişat: %2 faiz." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Cari səhifə." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "İstiqamət %1. Diqqət tələb olunur." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "İstiqamət %1." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Çəkməcəni bağlayın" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Çəkməcəni açın" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Geriyə hərəkət" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "İrəli hərəkət" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Bir çox işlər" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Keçidi mübadilə yaddaşına kopyalayın" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "%1 qrafik server platforması" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (%3 versiyadan yığılıb)" diff --git a/po/bg/libkirigami2plugin_qt.po b/po/bg/libkirigami2plugin_qt.po new file mode 100644 index 0000000..b87e4ea --- /dev/null +++ b/po/bg/libkirigami2plugin_qt.po @@ -0,0 +1,245 @@ +# Mincho Kondarev , 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: 2022-07-10 11:54+0200\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 22.04.2\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Посетете %1 на страницата на KDE магазин" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Изпращане на имейл до %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Включете се" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Авторско право" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Лиценз:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Лиценз: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Използвани библиотеки" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Автори" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Показване на снимки на автора" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Кредити" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Преводачи" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Относно %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Подаване на сигнал за грешка…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Изход" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Повече действия" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Премахване на етикет" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Действия" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Назад" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Затваряне на страничната лента" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Отваряне на страничната лента" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Зареждане…" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Парола" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Търсене…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Търсене" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Настройки" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Настройки — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Текуща страница. Прогрес: %1 %." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Навигиране до %1. Прогрес: %2 %." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Текуща страница." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Навигиране до %1. Изисква внимание." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Навигиране до %1." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Затваряне на чекмеджето" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Отваряне на чекмеджето" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Навигиране обратно" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Навигиране напред" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Повече действия" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Копиране на връзката в клипборда" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Система за прозорци %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (изграден върху %3)" diff --git a/po/ca/libkirigami2plugin_qt.po b/po/ca/libkirigami2plugin_qt.po new file mode 100644 index 0000000..6dbdc76 --- /dev/null +++ b/po/ca/libkirigami2plugin_qt.po @@ -0,0 +1,253 @@ +# Translation of libkirigami2plugin_qt.po to Catalan +# Copyright (C) 2016-2022 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. +# +# Antoni Bella Pérez , 2016, 2020, 2021. +# Josep M. Ferrer , 2017, 2018, 2019, 2020, 2021, 2022. +# Empar Montoro Martín , 2019. +msgid "" +msgstr "" +"Project-Id-Version: kirigami\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"PO-Revision-Date: 2022-07-07 09:32+0200\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 20.12.0\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Visita la pàgina %1 de la KDE Store" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Envia un correu a %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Col·laboreu-hi" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Drets d'autor" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Llicència:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Llicència: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Biblioteques en ús" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autors" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Mostra les fotos dels autors" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Atribucions" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Traductors" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Quant al %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Informa d'un error…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Surt" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Més accions" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Elimina l'etiqueta" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Accions" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Enrere" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Tanca la barra lateral" + +#: controls/GlobalDrawer.qml:594 +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:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Contrasenya" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Cerca…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Cerca" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Arranjament" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Configuració — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Pàgina actual. Progrés: %1 per cent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Navega a %1. Progrés: %2 per cent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Pàgina actual." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Navega a %1. Exigeix atenció." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Navega a %1." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Tanca el calaix" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Obre el calaix" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navega enrere" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navega endavant" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Més accions" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copia l'enllaç al porta-retalls" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "Frameworks %1 del KDE" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "El sistema de finestres %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (construïdes amb %3)" diff --git a/po/ca@valencia/libkirigami2plugin_qt.po b/po/ca@valencia/libkirigami2plugin_qt.po new file mode 100644 index 0000000..815fdb5 --- /dev/null +++ b/po/ca@valencia/libkirigami2plugin_qt.po @@ -0,0 +1,253 @@ +# Translation of libkirigami2plugin_qt.po to Catalan (Valencian) +# Copyright (C) 2016-2022 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. +# +# Antoni Bella Pérez , 2016, 2020, 2021. +# Josep M. Ferrer , 2017, 2018, 2019, 2020, 2021, 2022. +# Empar Montoro Martín , 2019. +msgid "" +msgstr "" +"Project-Id-Version: kirigami\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"PO-Revision-Date: 2022-07-07 09:32+0200\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 20.12.0\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Visita la pàgina %1 de la KDE Store" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Envia un correu a %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Col·laboreu-hi" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Drets d'autoria" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Llicència:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Llicència: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Biblioteques en ús" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autoria" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Mostra les fotos dels autors" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Atribucions" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Equip de traducció" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Quant a %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Informeu d'un error…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Ix" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Més accions" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Elimina l'etiqueta" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Accions" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Arrere" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Tanca la barra lateral" + +#: controls/GlobalDrawer.qml:594 +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:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Contrasenya" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Busca…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Busca" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Configuració" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Configuració — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Pàgina actual. Progrés: %1 per cent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Navega a %1. Progrés: %2 per cent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Pàgina actual." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Navega a %1. Exigix atenció." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Navega a %1." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Tanca el calaix" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Obri el calaix" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navega arrere" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navega avant" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Més accions" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copia l'enllaç al porta-retalls" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "Frameworks %1 de KDE" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "El sistema de finestres %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (construïdes amb %3)" diff --git a/po/cs/libkirigami2plugin_qt.po b/po/cs/libkirigami2plugin_qt.po new file mode 100644 index 0000000..ee3f6f9 --- /dev/null +++ b/po/cs/libkirigami2plugin_qt.po @@ -0,0 +1,245 @@ +# Vít Pelčák , 2016, 2017, 2018, 2019, 2020. +# Vit Pelcak , 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-05-19 11:28+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 22.04.1\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Navštívit stránku KDE Store od %1" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Odeslat e-mail na %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Přidejte se" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licence:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licence: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Použité knihovny" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autoři" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Zobrazit fotografie autora" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Poděkování" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Překladatelé" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "O aplikaci %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Nahlásit chybu…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Ukončit" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Další činnosti" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Odstranit značku" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Činnosti" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Zpět" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Zavřít postranní panel" + +#: controls/GlobalDrawer.qml:594 +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:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Heslo" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Hledat…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Hledat" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Nastavení" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Nastavení — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Současná stránka. Postup: %1 procent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Přejít na %1. Postup: %2 procent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Aktuální stránka." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Přejít na %1. Vyžadovat pozornost." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Přejít na %1." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Zavřít šuplík" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Otevřít šuplík" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Přejít zpět" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Přejít vpřed" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Další činnosti" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Okenní systém %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (sestaveno oproti %3)" diff --git a/po/da/libkirigami2plugin_qt.po b/po/da/libkirigami2plugin_qt.po new file mode 100644 index 0000000..3000f2a --- /dev/null +++ b/po/da/libkirigami2plugin_qt.po @@ -0,0 +1,279 @@ +# Martin Schlander , 2017, 2018, 2019, 2020. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2020-10-27 19:16+0100\n" +"Last-Translator: Martin Schlander \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 20.04.2\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "" + +#: controls/AboutItem.qml:139 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "Send an email to %1" +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Send en e-mail til %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:198 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Ophavsret" + +#: controls/AboutItem.qml:247 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "License:" +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licens:" + +#: controls/AboutItem.qml:269 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "License: %1" +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licens: %1" + +#: controls/AboutItem.qml:280 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Libraries in use" +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Biblioteker i brug" + +#: controls/AboutItem.qml:310 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Authors" +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Ophavsmænd" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:350 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Credits" +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Tak" + +#: controls/AboutItem.qml:363 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Translators" +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Oversættere" + +#: controls/AboutPage.qml:85 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "About" +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Om" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Flere handlinger" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Handlinger" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Tilbage" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Luk sidepanel" + +#: controls/GlobalDrawer.qml:594 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Luk sidepanel" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Adgangskode" + +#: controls/SearchField.qml:95 +#, fuzzy +#| msgctxt "SearchField|" +#| msgid "Search" +msgctxt "SearchField|" +msgid "Search…" +msgstr "Søg" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Søg" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, fuzzy, qt-format +#| msgctxt "ForwardButton|" +#| msgid "Navigate Forward" +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Navigér fremad" + +#: controls/templates/OverlayDrawer.qml:140 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Luk sidepanel" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navigér tilbage" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navigér fremad" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Flere handlinger" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Vinduessystemet %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (bygget op imod %3)" diff --git a/po/de/libkirigami2plugin_qt.po b/po/de/libkirigami2plugin_qt.po new file mode 100644 index 0000000..ce83f59 --- /dev/null +++ b/po/de/libkirigami2plugin_qt.po @@ -0,0 +1,246 @@ +# Frederik Schwarzer , 2016, 2018, 2020, 2021, 2022. +# Burkhard Lück , 2017, 2018, 2019, 2020, 2021. +# Alois Spitzbart , 2021. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-07-23 23:16+0200\n" +"Last-Translator: Frederik Schwarzer \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 22.07.70\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Besuchen Sie %1 im KDE Store" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Eine E-Mail an %1 senden" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Machen Sie mit" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Lizenz:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Lizenz: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Verwendete Bibliotheken" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autoren" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Fotos von Autoren anzeigen" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Danksagungen" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Übersetzer" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Über %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Probleme oder Wünsche berichten ..." + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Beenden" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Weitere Aktionen" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Stichwort entfernen" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Aktionen" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Zurück" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Seitenleiste schließen" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Seitenleiste öffnen" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Wird geladen ..." + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Passwort" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Suchen ..." + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Suchen" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Einstellungen" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Einstellungen — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Aktuelle Seite. Fortschritt: %1 Prozent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Zu %1 gehen. Fortschritt: %2 Prozent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Aktuelle Seite." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Zu %1 gehen. Erfordert Aufmerksamkeit." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Zu %1 gehen." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Seitenleiste schließen" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Seitenleiste öffnen" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Zurück gehen" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Vorwärts gehen" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Weitere Aktionen" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Verknüpfung in die Zwischenablage kopieren" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Das Fenstersystem %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (kompiliert gegen %3)" diff --git a/po/el/libkirigami2plugin_qt.po b/po/el/libkirigami2plugin_qt.po new file mode 100644 index 0000000..29c5643 --- /dev/null +++ b/po/el/libkirigami2plugin_qt.po @@ -0,0 +1,250 @@ +# 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" +"X-Qt-Contexts: true\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 20.04.2\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Επισκεφθείτε τη σελίδα της αποθήκης του KDE για το %1" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Αποστολή μηνύματος στο %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Ασχοληθείτε" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Άδεια χρήσης:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Άδεια χρήσης: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Βιβλιοθήκες σε χρήση" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Συγγραφείς" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Εμφάνιση φωτογραφιών συγγραφέων" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Ευχαριστίες" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Μεταφραστές" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Περίγραμμα %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Αναφορά σφάλματος…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Έξοδος" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Περισσότερες ενέργειες" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Ενέργειες" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Πίσω" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Κλείσιμο πλευρικής γραμμής" + +#: controls/GlobalDrawer.qml:594 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Κλείσιμο πλευρικής γραμμής" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Κωδικός πρόσβασης" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Αναζήτηση…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Αναζήτηση" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Ρυθμίσεις" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Ρυθμίσεις — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Τρέχουσα σελίδα. Πρόοδος: %1 τοις εκατό." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Πλοήγηση προς το %1. Πρόοδος: %2 τοις εκατό." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Τρέχουσα σελίδα." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Πλοήγηση προς το %1. Δώστε προσοχή." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Πλοήγηση προς το %1." + +#: controls/templates/OverlayDrawer.qml:140 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Κλείσιμο πλευρικής γραμμής" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Πλοήγηση προς τα πίσω" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Πλοήγηση προς τα εμπρός" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Περισσότερες ενέργειες" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Το %1 παραθυρικό σύστημα" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (κατασκευάστηκε με βάση το %3)" diff --git a/po/en_GB/libkirigami2plugin_qt.po b/po/en_GB/libkirigami2plugin_qt.po new file mode 100644 index 0000000..76da5d9 --- /dev/null +++ b/po/en_GB/libkirigami2plugin_qt.po @@ -0,0 +1,244 @@ +# Steve Allewell , 2016, 2017, 2018, 2019, 2020, 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-07-22 14:41+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 22.04.3\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Visit %1's KDE Store page" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Send an email to %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Get Involved" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licence:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licence: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Libraries in use" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Authors" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Show author photos" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Credits" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Translators" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "About %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Report Bug…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Quit" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "More Actions" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Remove Tag" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Actions" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Back" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Close Sidebar" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Open Sidebar" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Loading…" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Password" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Search…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Search" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Settings" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Settings — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Current page. Progress: %1 percent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Navigate to %1. Progress: %2 percent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Current page." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Navigate to %1. Demanding attention." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Navigate to %1." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Close drawer" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Open drawer" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navigate Back" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navigate Forward" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "More Actions" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copy Link to Clipboard" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "The %1 windowing system" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (built against %3)" diff --git a/po/es/libkirigami2plugin_qt.po b/po/es/libkirigami2plugin_qt.po new file mode 100644 index 0000000..7a15803 --- /dev/null +++ b/po/es/libkirigami2plugin_qt.po @@ -0,0 +1,244 @@ +# Eloy Cuadra , 2016, 2017, 2018, 2019, 2020, 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: libkirigamiplugin_qt\n" +"PO-Revision-Date: 2022-07-08 02:13+0200\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 22.04.2\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Visitar la página de %1 en KDE Store" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Enviar un correo electrónico a %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Involucrarse" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licencia:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licencia: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Bibliotecas en uso" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autores" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Mostrar fotos del autor" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Créditos" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Traductores" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Acerca de %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Informar de fallo..." + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Salir" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Más acciones" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Eliminar etiqueta" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Acciones" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Atrás" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Cerrar la barra lateral" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Abrir la barra lateral" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Cargando..." + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Contraseña" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Buscar..." + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Buscar" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Preferencias" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Preferencias — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Página actual. Avance: %1 por ciento." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Navegar hasta %1. Avance: %2 por ciento." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Página actual." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Navegar hasta %1. Solicitando atención." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Navegar hasta %1." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Cerrar cajón" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Abrir cajón" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Retroceder una página" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Avanzar una página" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Más acciones" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copiar enlace en el portapapeles" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "El sistema de ventanas %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (compilado para %3)" diff --git a/po/et/libkirigami2plugin_qt.po b/po/et/libkirigami2plugin_qt.po new file mode 100644 index 0000000..74bfa4d --- /dev/null +++ b/po/et/libkirigami2plugin_qt.po @@ -0,0 +1,280 @@ +# 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:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "" + +#: controls/AboutItem.qml:139 +#, 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:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:198 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Autoriõigus" + +#: controls/AboutItem.qml:247 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "License:" +msgctxt "AboutItem|" +msgid "License:" +msgstr "Litsents:" + +#: controls/AboutItem.qml:269 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "License: %1" +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Litsents: %1" + +#: controls/AboutItem.qml:280 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Libraries in use" +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Kasutatavad teegid" + +#: controls/AboutItem.qml:310 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Authors" +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autorid" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:350 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Credits" +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Tunnustus" + +#: controls/AboutItem.qml:363 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Translators" +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Tõlkijad" + +#: controls/AboutPage.qml:85 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "About" +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Teave" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Veel toiminguid" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Toimingud" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Tagasi" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Sulge külgriba" + +#: controls/GlobalDrawer.qml:594 +#, 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:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Parool" + +#: controls/SearchField.qml:95 +#, fuzzy +#| msgctxt "SearchField|" +#| msgid "Search" +msgctxt "SearchField|" +msgid "Search…" +msgstr "Otsi" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Otsi" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, fuzzy, qt-format +#| msgctxt "ForwardButton|" +#| msgid "Navigate Forward" +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Liigu edasi" + +#: controls/templates/OverlayDrawer.qml:140 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Sulge külgriba" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Liigu tagasi" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Liigu edasi" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Veel toiminguid" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "%1 aknahaldur" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (ehitatud %3 peale)" diff --git a/po/eu/libkirigami2plugin_qt.po b/po/eu/libkirigami2plugin_qt.po new file mode 100644 index 0000000..6dde139 --- /dev/null +++ b/po/eu/libkirigami2plugin_qt.po @@ -0,0 +1,251 @@ +# Translation for libkirigami2plugin_qt.po to Euskara/Basque (eu). +# Copyright (C) 2017-2018, Free Software Foundation. +# Copyright (C) 2019-2022, This file is copyright: +# This file is distributed under the same license as the original file. +# KDE euskaratzeko proiektuko arduraduna . +# +# Translators: +# Iñigo Salvador Azurmendi , 2017, 2018, 2019, 2020, 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: kirigami\n" +"PO-Revision-Date: 2022-08-05 18:19+0200\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 22.04.3\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Bisitatu %1(r)(e)n KDE Biltegiko orria" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Bidali e-posta honi: %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Engaia zaitez" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Lizentzia:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Lizentzia: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Erabiltzen ari diren liburutegiak" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Egileak" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Erakutsi egilearen argazkiak" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Merituak" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Itzultzaileak" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1(e)ri buruz" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Akatsa jakinarazi..." + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Irten" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Ekintza gehiago" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Kendu etiketa" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Ekintzak" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Atzera" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Itxi alboko-barra" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Itxi alboko-barra" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Zamatzen…" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Pasahitza" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Bilatu..." + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Bilatu..." + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Ezarpenak" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Ezarpenak — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Uneko orria. Aurrerapena: ehuneko %1." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Nabigatu %1(e)ra. Aurrerapena: ehuneko %2." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Uneko orria." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Nabigatu %1(e)ra. Arreta eskatuz." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Nabigatu %1(e)ra." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Itxi tiradera" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Ireki tiradera" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Nabigatu atzera" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Nabigatu aurrera" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Ekintza gehiago" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Kopiatu esteka arbelera" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "%1 leiho-sistema" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (%3 erabiliz eraikia)" diff --git a/po/fi/libkirigami2plugin_qt.po b/po/fi/libkirigami2plugin_qt.po new file mode 100644 index 0000000..72350c6 --- /dev/null +++ b/po/fi/libkirigami2plugin_qt.po @@ -0,0 +1,248 @@ +# Lasse Liehu , 2017. +# Tommi Nieminen , 2018, 2019, 2020, 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-05-18 19:12+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 20.04.2\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Käy KDE Storen %1 -sivulla" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Lähetä sähköpostia: %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Ota osaa" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Tekijänoikeudet" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Lisenssi:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Lisenssi: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Käytetyt kirjastot" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Tekijät" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Näytä tekijöiden valokuvat" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Kiitokset" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Kääntäjät" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Tietoa – %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Ilmoita viasta…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Lopeta" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Lisää toimintoja" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Poista luokitus" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Toiminnot" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Takaisin" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Sulje sivupaneeli" + +#: controls/GlobalDrawer.qml:594 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Sulje sivupaneeli" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Ladataan…" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Salasana" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Etsi…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Etsi" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Asetukset" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Asetukset – %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Nykyinen sivu. Edistyminen: %1 %." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Siirry kohteeseen %1. Edistyminen: %2 %." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Nykyinen sivu." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Siirry kohteeseen %1. Vaatii huomiota." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Siirry kohteeseen %1." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Sulje laatikko" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Avaa laatikko" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Siirry taaksepäin" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Siirry eteenpäin" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Lisää toimintoja" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Ikkunointijärjestelmä %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (koostettu kirjastolla %3)" diff --git a/po/fr/libkirigami2plugin_qt.po b/po/fr/libkirigami2plugin_qt.po new file mode 100644 index 0000000..322c27c --- /dev/null +++ b/po/fr/libkirigami2plugin_qt.po @@ -0,0 +1,249 @@ +# Vincent Pinon , 2016, 2017. +# Simon Depiets , 2018, 2019. +# Xavier Besnard , 2020, 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-07-08 09:47+0200\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-Generator: Lokalize 22.04.2\n" +"X-Environment: kde\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: qtrich\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Visiter la page de la boutique de KDE pour %1" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Envoyer un courriel à %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Soyez impliqué" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Droit d'auteur" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licence :" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licence : %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Bibliothèques en cours d'utilisation" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Auteurs" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Afficher les photos des auteurs" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Crédits" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Traducteurs" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "À propos de %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Signaler un bogue…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Quitter" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Plus d'actions" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Supprimer une étiquette" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Actions" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Retour" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Fermer la barre latérale" + +#: controls/GlobalDrawer.qml:594 +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:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Mot de passe" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Rechercher…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Rechercher" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Configuration" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Configuration — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Page actuelle. Avancement : %1 pourcent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Naviguer vers %1. Avancement : %2 pourcent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Page actuelle." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Naviguer vers %1. Attention requise." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Navigation vers %1" + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Fermer un tiroir" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Ouvrir un tiroir" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navigation arrière" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navigation avant" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Actions supplémentaires" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copier un lien dans le presse-papier" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Le système de fenêtres %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (compilé avec %3)" diff --git a/po/gl/libkirigami2plugin_qt.po b/po/gl/libkirigami2plugin_qt.po new file mode 100644 index 0000000..080735f --- /dev/null +++ b/po/gl/libkirigami2plugin_qt.po @@ -0,0 +1,278 @@ +# Adrián Chaves Fernández (Gallaecio) , 2017. +# Adrián Chaves (Gallaecio) , 2018, 2019. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2019-11-26 23:14+0100\n" +"Last-Translator: Adrián Chaves (Gallaecio) \n" +"Language-Team: Galician \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 19.08.3\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:198 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Dereitos de copia" + +#: controls/AboutItem.qml:247 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "License:" +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licenza:" + +#: controls/AboutItem.qml:269 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "License: %1" +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licenza: %1" + +#: controls/AboutItem.qml:280 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Libraries in use" +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Bibliotecas usadas" + +#: controls/AboutItem.qml:310 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Authors" +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autores" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:350 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Credits" +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Recoñecementos" + +#: controls/AboutItem.qml:363 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Translators" +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Tradutores" + +#: controls/AboutPage.qml:85 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "About" +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Sobre" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:204 +#, fuzzy +#| msgctxt "ToolBarApplicationHeader|" +#| msgid "More Actions" +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Máis accións" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Accións" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Atrás" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Contrasinal" + +#: controls/SearchField.qml:95 +#, fuzzy +#| msgctxt "SearchField|" +#| msgid "Search..." +msgctxt "SearchField|" +msgid "Search…" +msgstr "Buscar…" + +#: controls/SearchField.qml:97 +#, fuzzy +#| msgctxt "SearchField|" +#| msgid "Search..." +msgctxt "SearchField|" +msgid "Search" +msgstr "Buscar…" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, fuzzy, qt-format +#| msgctxt "ForwardButton|" +#| msgid "Navigate Forward" +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Continuar" + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Volver" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Continuar" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Máis accións" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "Versión %1 das infraestruturas de KDE" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "O sistema de xanelas %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (construído sobre %3)" diff --git a/po/hi/libkirigami2plugin_qt.po b/po/hi/libkirigami2plugin_qt.po new file mode 100644 index 0000000..0397b8a --- /dev/null +++ b/po/hi/libkirigami2plugin_qt.po @@ -0,0 +1,282 @@ +# 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:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:130 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "Visit %1's KDE Store page" +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "%1 के केडीई स्टोर पृष्ठ पर जाएँ" + +#: controls/AboutItem.qml:139 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "Send an email to %1" +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "%1 को ईमेल भेजें" + +#: controls/AboutItem.qml:186 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Get Involved" +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "सहभाग लें" + +#: controls/AboutItem.qml:198 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "सर्वाधिकार" + +#: controls/AboutItem.qml:247 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "License:" +msgctxt "AboutItem|" +msgid "License:" +msgstr "अनुज्ञापत्र :" + +#: controls/AboutItem.qml:269 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "License: %1" +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "अनुज्ञापत्र : %1" + +#: controls/AboutItem.qml:280 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Libraries in use" +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "उपयोग किए गए संग्रह" + +#: controls/AboutItem.qml:310 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Authors" +msgctxt "AboutItem|" +msgid "Authors" +msgstr "लेखक" + +#: controls/AboutItem.qml:319 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Show author photos" +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "लेखक की फोटोओं को दिखाएँ" + +#: controls/AboutItem.qml:350 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Credits" +msgctxt "AboutItem|" +msgid "Credits" +msgstr "आभार सूची" + +#: controls/AboutItem.qml:363 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Translators" +msgctxt "AboutItem|" +msgid "Translators" +msgstr "अनुवादक" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1 के बारे में" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "बाहर जाएँ" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "अधिक क्रियाएँ" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "क्रियाएं" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "पीछे" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "बाजूपट्टी बंद करें" + +#: controls/GlobalDrawer.qml:594 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "बाजूपट्टी बंद करें" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "कूटशब्द" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "खोजें…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "खोजें" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "विन्यास" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, fuzzy, qt-format +#| msgctxt "CategorizedSettings|" +#| msgid "Settings" +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "विन्यास" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "वर्तमान पृष्ठ। प्रगती : %1 प्रतिशत।" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "%1 पर जाएँ। प्रगती : %2 प्रतिशत।" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "वर्तमान पृष्ठ।" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "%1 पर जाएँ। ध्यान देने की मांग कर रहा है।" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "%1 पर जाएँ।" + +#: controls/templates/OverlayDrawer.qml:140 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "बाजूपट्टी बंद करें" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "पीछे जाएँ" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "आगे जाएँ" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "अधिक क्रियाएँ" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "केडीई फ्रेमवर्कस %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "%1 विंडो प्रणाली" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "क्यूट %2 (%3 के प्रती निर्मित)" diff --git a/po/hu/libkirigami2plugin_qt.po b/po/hu/libkirigami2plugin_qt.po new file mode 100644 index 0000000..9d9fd6f --- /dev/null +++ b/po/hu/libkirigami2plugin_qt.po @@ -0,0 +1,251 @@ +# Kiszel Kristóf , 2017, 2018, 2020, 2021. +# Kristof Kiszel , 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-02-09 17:29+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 21.07.70\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Ugrás a(z) %1 KDE Store oldalára" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "E-mail küldése neki: %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Közreműködés" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licenc:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licenc: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Felhasznált függvénykönyvtárak" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Szerzők" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "A szerző fényképének megjelenítése" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Köszönetnyilvánítás" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Fordítók" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Névjegy: %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Hibabejelentés…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Kilépés" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "További műveletek" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Címke eltávolítása" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Műveletek" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Vissza" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Oldalsáv bezárása" + +#: controls/GlobalDrawer.qml:594 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Oldalsáv bezárása" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Jelszó" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Keresés…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Keresés" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Beállítások" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Beállítások — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Jelenlegi oldal. Folyamat: %1 százalék." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Ugrás ide: %1. Folyamat. %2 százalék." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Jelenlegi oldal." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Ugrás ide: %1. Beavatkozást igényel." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Ugrás ide: %1." + +#: controls/templates/OverlayDrawer.qml:140 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Oldalsáv bezárása" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Vissza" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Előre" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "További műveletek" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "A(z) %1 ablakkezelő rendszer" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (fordítva ezzel: %3)" diff --git a/po/ia/libkirigami2plugin_qt.po b/po/ia/libkirigami2plugin_qt.po new file mode 100644 index 0000000..afdc948 --- /dev/null +++ b/po/ia/libkirigami2plugin_qt.po @@ -0,0 +1,244 @@ +# giovanni , 2017, 2019, 2020, 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-07-15 11:53+0200\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 21.12.3\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Visita pagina %1 de KDE Store" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Invia un message de e-posta a %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Sea involvite" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licentia:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licentia: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Bibliothecas in uso" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autores" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Monstra photos de autor" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Gratias" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Traductores" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "A proposito de %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Reporta Bug…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Abandona" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Ulterior Actiones" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Remove etiquetta" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Actiones" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Retro" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Claude barra lateral" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Aperi barra lateral" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Cargante..." + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Contrasigno" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Cerca…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Cerca" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Preferentias" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Preferentias — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Pagina currente. Progresso: %1 percent" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Naviga a %1. Progresso: %2 percent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Pagina currente." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Navigante a %1. Demandante attention." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Naviga a %1" + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Claude designator" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Aperi designator" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navigation de retro" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Naviga Avante" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Ulterior Actiones" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copia ligame a area de transferentia" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Le %1 systema de fenestra" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (construite sur %3)" diff --git a/po/id/libkirigami2plugin_qt.po b/po/id/libkirigami2plugin_qt.po new file mode 100644 index 0000000..98dd78e --- /dev/null +++ b/po/id/libkirigami2plugin_qt.po @@ -0,0 +1,283 @@ +# Wantoyo , 2018, 2019, 2020, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-08-19 20:47+0700\n" +"Last-Translator: Wantoyèk \n" +"Language-Team: https://t.me/Localizations_KDE_Indonesia\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:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:198 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Hakcipta" + +#: controls/AboutItem.qml:247 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "License:" +msgctxt "AboutItem|" +msgid "License:" +msgstr "Lisensi:" + +#: controls/AboutItem.qml:269 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "License: %1" +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Lisensi: %1" + +#: controls/AboutItem.qml:280 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Libraries in use" +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Pustaka Lib yang digunakan" + +#: controls/AboutItem.qml:310 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Authors" +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Penulis" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:350 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Credits" +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Pujian" + +#: controls/AboutItem.qml:363 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Translators" +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Penerjemah" + +#: controls/AboutPage.qml:85 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "About" +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Tentang" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:204 +#, fuzzy +#| msgctxt "ToolBarApplicationHeader|" +#| msgid "More Actions" +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Aksi Selebihnya" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Aksi" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Mundur" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Tutup Bilah Sisi" + +#: controls/GlobalDrawer.qml:594 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Tutup BilahSisi" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Password" + +#: controls/SearchField.qml:95 +#, fuzzy +#| msgctxt "SearchField|" +#| msgid "Search..." +msgctxt "SearchField|" +msgid "Search…" +msgstr "Cari..." + +#: controls/SearchField.qml:97 +#, fuzzy +#| msgctxt "SearchField|" +#| msgid "Search..." +msgctxt "SearchField|" +msgid "Search" +msgstr "Cari..." + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, fuzzy, qt-format +#| msgctxt "ForwardButton|" +#| msgid "Navigate Forward" +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Navigasi Maju" + +#: controls/templates/OverlayDrawer.qml:140 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Tutup BilahSisi" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navigasi Mundur" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navigasi Maju" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Aksi Selebihnya" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Sistem windowing %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (dibangun terhadap %3)" diff --git a/po/it/libkirigami2plugin_qt.po b/po/it/libkirigami2plugin_qt.po new file mode 100644 index 0000000..6305178 --- /dev/null +++ b/po/it/libkirigami2plugin_qt.po @@ -0,0 +1,244 @@ +# Vincenzo Reale , 2017, 2018, 2019, 2020, 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-07-09 19:41+0200\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 22.04.3\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Visita la pagina del KDE Store di %1" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Invia un messaggio di posta elettronica a %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Partecipa" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licenza:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licenza: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Librerie in uso" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autori" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Mostra le foto degli autori" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Riconoscimenti" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Traduttori" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Informazioni su %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Segnala bug…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Esci" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Altre azioni" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Rimuovi etichetta" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Azioni" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Indietro" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Chiudi la barra laterale" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Apri la barra laterale" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Caricamento…" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Password" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Cerca…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Cerca" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Impostazioni" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Impostazioni — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Pagina attuale. Avanzamento: %1 percento." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Vai a %1. Avanzamento: %2 percento." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Pagina attuale." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Vai a %1. Richiede attenzione." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Vai a %1." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Chiudi il cassetto" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Apri il cassetto" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Naviga indietro" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Naviga in avanti" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Altre azioni" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copia il collegamento negli appunti" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Il sistema di gestione delle finestre %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (compilato con %3)" diff --git a/po/ja/libkirigami2plugin_qt.po b/po/ja/libkirigami2plugin_qt.po new file mode 100644 index 0000000..037af5c --- /dev/null +++ b/po/ja/libkirigami2plugin_qt.po @@ -0,0 +1,242 @@ +msgid "" +msgstr "" +"Project-Id-Version: libkirigamiplugin_qt\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=2; plural=n != 1;\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: qtrich\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "" + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "" diff --git a/po/ko/libkirigami2plugin_qt.po b/po/ko/libkirigami2plugin_qt.po new file mode 100644 index 0000000..5474aee --- /dev/null +++ b/po/ko/libkirigami2plugin_qt.po @@ -0,0 +1,244 @@ +# Shinjo Park , 2017, 2018, 2019, 2020, 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-07-13 12:50+0200\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 21.12.3\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "%1의 KDE 스토어 페이지 방문" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "%1(으)로 이메일 보내기" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "참여하기" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "저작권" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "라이선스:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "라이선스: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "사용하는 라이브러리" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "작성자" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "작성자 사진 표시" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "제작진" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "번역자" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1 정보" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "버그 보고…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "끝내기" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "더 많은 동작" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "태그 삭제" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "동작" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "뒤로" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "사이드바 닫기" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "사이드바 열기" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "불러오는 중…" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "암호" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "검색…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "검색" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "설정" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "설정 — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "현재 쪽입니다. 진행 상황: %1퍼센트." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "%1(으)로 탐색합니다. 진행 상황: %2퍼센트." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "현재 쪽입니다." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "%1(으)로 탐색합니다. 주목을 기다립니다." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "%1(으)로 탐색합니다." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "서랍 닫기" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "서랍 열기" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "이전 탐색" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "다음 탐색" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "더 많은 동작" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "클립보드에 링크 복사" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE 프레임워크 %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "%1 창 시스템" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2(%3(으)로 빌드됨)" diff --git a/po/lt/libkirigami2plugin_qt.po b/po/lt/libkirigami2plugin_qt.po new file mode 100644 index 0000000..714ea69 --- /dev/null +++ b/po/lt/libkirigami2plugin_qt.po @@ -0,0 +1,284 @@ +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" +"X-Qt-Contexts: true\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-Generator: Poedit 2.2.4\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:198 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Autorių teisės" + +#: controls/AboutItem.qml:247 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "License:" +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licencija:" + +#: controls/AboutItem.qml:269 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "License: %1" +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licencija: %1" + +#: controls/AboutItem.qml:280 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Libraries in use" +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Naudojamos bibliotekos" + +#: controls/AboutItem.qml:310 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Authors" +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autoriai" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:350 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Credits" +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Padėkos" + +#: controls/AboutItem.qml:363 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Translators" +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Vertėjai" + +#: controls/AboutPage.qml:85 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "About" +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Apie" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:204 +#, fuzzy +#| msgctxt "ToolBarApplicationHeader|" +#| msgid "More Actions" +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Daugiau veiksmų" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Veiksmai" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Atgal" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Užverti šoninę juostą" + +#: controls/GlobalDrawer.qml:594 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Užverti šoninę juostą" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Slaptažodis" + +#: controls/SearchField.qml:95 +#, fuzzy +#| msgctxt "SearchField|" +#| msgid "Search..." +msgctxt "SearchField|" +msgid "Search…" +msgstr "Ieškoti..." + +#: controls/SearchField.qml:97 +#, fuzzy +#| msgctxt "SearchField|" +#| msgid "Search..." +msgctxt "SearchField|" +msgid "Search" +msgstr "Ieškoti..." + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, fuzzy, qt-format +#| msgctxt "ForwardButton|" +#| msgid "Navigate Forward" +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Naršyti pirmyn" + +#: controls/templates/OverlayDrawer.qml:140 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Užverti šoninę juostą" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Naršyti atgal" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Naršyti pirmyn" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Daugiau veiksmų" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "%1 langų sistema" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (sudaryta remiantis %3)" diff --git a/po/ml/libkirigami2plugin_qt.po b/po/ml/libkirigami2plugin_qt.po new file mode 100644 index 0000000..a8e2ec3 --- /dev/null +++ b/po/ml/libkirigami2plugin_qt.po @@ -0,0 +1,242 @@ +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:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "" + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "" diff --git a/po/nl/libkirigami2plugin_qt.po b/po/nl/libkirigami2plugin_qt.po new file mode 100644 index 0000000..7d43837 --- /dev/null +++ b/po/nl/libkirigami2plugin_qt.po @@ -0,0 +1,244 @@ +# Freek de Kruijf , 2016, 2017, 2018, 2019, 2020, 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-07-07 13:15+0200\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 22.04.2\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Bezoek de KDE Store-pagina van %1" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Een e-mail sturen naar %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Doe mee" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licentie:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licentie: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Bibliotheken in gebruik" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Auteurs" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Foto's van auteurs tonen" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Dankbetuigingen" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Vertalers" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Info over %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Bug rapporteren…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Afsluiten" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Meer acties" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Tag verwijderen" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Acties" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Terug" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Zijbalk sluiten" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Zijbalk openen" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Bezig met laden…" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Wachtwoord" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Zoeken…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Zoeken" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Instellingen" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Instellingen — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Huidige pagina. Voortgang: %1 procent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Naar %1 navigeren. Voortgang: %2 procent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Huidige pagina." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Naar %1 navigeren. Vraagt om aandacht." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Naar %1 navigeren." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Schuiflade sluiten" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Schuiflade openen" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Naar achteren navigeren" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Naar voren navigeren" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Meer acties" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Koppeling naar klembord kopiëren" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Het venstersysteem %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (gebouwd tegen %3)" diff --git a/po/nn/libkirigami2plugin_qt.po b/po/nn/libkirigami2plugin_qt.po new file mode 100644 index 0000000..28108e5 --- /dev/null +++ b/po/nn/libkirigami2plugin_qt.po @@ -0,0 +1,249 @@ +# Translation of libkirigami2plugin_qt to Norwegian Nynorsk +# +# Karl Ove Hufthammer , 2016, 2018, 2019, 2020, 2021. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2021-03-21 17:44+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 20.12.3\n" +"X-Environment: kde\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: qtrich\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Avslutt" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Fleire handlingar" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 – %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Handlingar" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Tilbake" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Lukk sidestolpen" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Passord" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Søk" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "" + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Naviger tilbake" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Naviger fram" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Fleire handlingar" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Vindaugssystemet %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (bygd mot %3)" diff --git a/po/pa/libkirigami2plugin_qt.po b/po/pa/libkirigami2plugin_qt.po new file mode 100644 index 0000000..42ca07c --- /dev/null +++ b/po/pa/libkirigami2plugin_qt.po @@ -0,0 +1,288 @@ +# A S Alam , 2021. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2021-04-25 13:00-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 20.08.1\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "" + +#: controls/AboutItem.qml:139 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "Send an email to %1" +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "%1 ਨੂੰ ਈਮੇਲ ਭੇਜੋ" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:198 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "ਕਾਪੀਰਾਈਟ" + +#: controls/AboutItem.qml:247 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "License:" +msgctxt "AboutItem|" +msgid "License:" +msgstr "ਲਸੰਸ:" + +#: controls/AboutItem.qml:269 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "License: %1" +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "ਲਸੰਸ: %1" + +#: controls/AboutItem.qml:280 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Libraries in use" +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "ਵਰਤਣ ਲਈ ਲਾਇਬਰੇਰੀਆਂ" + +#: controls/AboutItem.qml:310 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Authors" +msgctxt "AboutItem|" +msgid "Authors" +msgstr "ਲੇਖਕ" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:350 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Credits" +msgctxt "AboutItem|" +msgid "Credits" +msgstr "ਮਾਣ" + +#: controls/AboutItem.qml:363 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Translators" +msgctxt "AboutItem|" +msgid "Translators" +msgstr "ਉਲੱਥਾਕਾਰ" + +#: controls/AboutPage.qml:85 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "About" +msgctxt "AboutPage|" +msgid "About %1" +msgstr "ਇਸ ਬਾਰੇ" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "ਬਾਹਰ" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "ਹੋਰ ਕਾਰਵਾਈਆਂ" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "ਕਾਰਵਾਈਆਂ" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "ਪਿੱਛੇ" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "ਬਾਹੀ ਬੰਦ ਕਰੋ" + +#: controls/GlobalDrawer.qml:594 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "ਬਾਹੀ ਬੰਦ ਕਰੋ" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "ਪਾਸਵਰਡ" + +#: controls/SearchField.qml:95 +#, fuzzy +#| msgctxt "SearchField|" +#| msgid "Search" +msgctxt "SearchField|" +msgid "Search…" +msgstr "ਖੋਜੋ" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "ਖੋਜੋ" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, fuzzy, qt-format +#| msgctxt "PrivateSwipeTab|" +#| msgid "Current page. Progress: %1 percent." +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "ਮੌਜੂਦਾ ਸਫ਼ਾ। ਤਰੱਕੀ: %1 ਫ਼ੀਸਦੀ" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, fuzzy, qt-format +#| msgctxt "PrivateSwipeTab|" +#| msgid "Navigate to %1. Progress: %2 percent." +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "%1 ਉੱਤੇ ਜਾਓ। ਤਰੱਕੀ: %2 ਫ਼ੀਸਦੀ" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +#, fuzzy +#| msgctxt "PrivateSwipeTab|" +#| msgid "Current page." +msgctxt "PageTab|" +msgid "Current page." +msgstr "ਮੌਜੂਦਾ ਸਫ਼ਾ।" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, fuzzy, qt-format +#| msgctxt "PrivateSwipeTab|" +#| msgid "Navigate to %1. Demanding attention." +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "%1 ਉੱਤੇ ਜਾਓ। ਧਿਆਨ ਦੇਣ ਦੀ ਲੋੜ ਹੈ।" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, fuzzy, qt-format +#| msgctxt "PrivateSwipeTab|" +#| msgid "Navigate to %1." +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "%1 ਉੱਤੇ ਜਾਓ।" + +#: controls/templates/OverlayDrawer.qml:140 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "ਬਾਹੀ ਬੰਦ ਕਰੋ" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "ਪਿੱਛੇ ਜਾਓ" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "ਅੱਗੇ ਜਾਓ" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "ਹੋਰ ਕਾਰਵਾਈਆਂ" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE ਫਰੇਮਵਰਕਸ %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "%1 ਵਿੰਡੋਇੰਗ ਸਿਸਟਮ" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (%3 ਨਾਲ ਬਣਾਇਆ)" diff --git a/po/pl/libkirigami2plugin_qt.po b/po/pl/libkirigami2plugin_qt.po new file mode 100644 index 0000000..f745ca7 --- /dev/null +++ b/po/pl/libkirigami2plugin_qt.po @@ -0,0 +1,245 @@ +# Łukasz Wojniłowicz , 2016, 2017, 2018, 2019, 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-07-31 08:55+0200\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" +"X-Qt-Contexts: true\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 22.03.70\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Odwiedź stronę sklepu KDE %1" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Wyślij wiadomość do %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Współtwórz" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Prawa autorskie" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licencja:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licencja: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Używane biblioteki" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autorzy" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Pokaż zdjęcia autorów" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Zasługi" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Tłumacze" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "O %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Zgłoś błąd..." + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Zakończ" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Więcej działań" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Usuń znacznik" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Działania" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Wstecz" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Zamknij pasek boczny" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Otwórz pasek boczny" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Wczytywanie..." + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Hasło" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Szukaj..." + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Szukaj" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Ustawienia" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Ustawienia — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Bieżąca strona. Postęp: %1 procent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Przejdź do %1. Postęp: %2 procent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Bieżąca strona." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Przejdź do %1. Wymaga uwagi." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Przejdź do %1." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Zamknij szufladę" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Otwórz szufladę" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Przejdź wstecz" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Przejdź naprzód" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Więcej działań" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Skopiuj odnośnik do schowka" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "Szkielety KDE %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "System okien %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (zbudowany na %3)" diff --git a/po/pt/libkirigami2plugin_qt.po b/po/pt/libkirigami2plugin_qt.po new file mode 100644 index 0000000..6213202 --- /dev/null +++ b/po/pt/libkirigami2plugin_qt.po @@ -0,0 +1,246 @@ +# 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-07-07 11:09+0100\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:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Visitar a página do %1 na Loja do KDE" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Enviar um e-mail para %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Envolva-se" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "'Copyright'" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licença:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licença: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Bibliotecas usadas" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autores" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Mostrar as fotografias dos autores" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Créditos" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Tradutores" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Acerca do %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Comunicar um Erro…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Sair" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Mais Acções" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Remover a Marca" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Acções" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Recuar" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Fechar a Barra Lateral" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Abrir a Barra Lateral" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "A carregar…" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Senha" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Procurar…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Procurar" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Configuração" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Configuração ̣̣— %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Página actual. Progresso: %1 por cento." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Navegar para %1. Progresso: %2 por cento." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Página actual." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Navegar para %1. A chamar a atenção." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Navegar para %1." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Fechar a área" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Abrir a área" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navegar para Trás" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navegar para a Frente" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Mais Acções" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copiar a Ligação para a Área de Transferência" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "Plataformas do KDE %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "O sistema de janelas %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (compilado com o %3)" diff --git a/po/pt_BR/libkirigami2plugin_qt.po b/po/pt_BR/libkirigami2plugin_qt.po new file mode 100644 index 0000000..da1bc63 --- /dev/null +++ b/po/pt_BR/libkirigami2plugin_qt.po @@ -0,0 +1,254 @@ +# 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. +# André Marcelo Alvarenga , 2018, 2019. +# Thiago Masato Costa Sueto , 2021. +msgid "" +msgstr "" +"Project-Id-Version: libkirigami2plugin_qt\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"PO-Revision-Date: 2022-07-12 16:26-0300\n" +"Last-Translator: Luiz Fernando Ranghetti \n" +"Language-Team: 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 21.12.3\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Visite a página do %1 na KDE Store" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Enviar e-mail para %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Participe" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licença:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licença: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Bibliotecas em uso" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autores" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Mostrar as fotos dos autores" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Créditos" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Tradutores" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Sobre %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Relatar erro..." + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Sair" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Mais ações" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Remover etiqueta" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Ações" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Voltar" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Fechar barra lateral" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Abrir barra lateral" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Carregando..." + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Senha" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Pesquisar..." + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Pesquisar" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Configurações" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Configurações — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Página atual. Progresso: %1 porcento." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Navegar para %1. Progresso: %2 porcento." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Página atual." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Navegar para %1. Demandando atenção." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Navegar para %1." + +#: controls/templates/OverlayDrawer.qml:140 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Fechar barra lateral" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Voltar" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Avançar" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Mais ações" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Copiar link para a área de transferência" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "O sistema de janelas %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (compilado com %3)" diff --git a/po/ro/libkirigami2plugin_qt.po b/po/ro/libkirigami2plugin_qt.po new file mode 100644 index 0000000..8140ede --- /dev/null +++ b/po/ro/libkirigami2plugin_qt.po @@ -0,0 +1,245 @@ +# Sergiu Bivol , 2020, 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-06-01 15:32+0100\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:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Vizitează pagina %1 din Magazinul KDE" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Trimite scrisoare către %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Implică-te" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Drept de autor" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licență:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licență: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Biblioteci folosite" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autori" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Arată pozele autorilor" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Mulțumiri" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Traducători" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Despre %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Raportează defect…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Termină" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Acțiuni suplimentare" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Elimină marcajul" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Acțiuni" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Înapoi" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Închide bara laterală" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Deschide bara laterală" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Se încarcă…" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Parolă" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Caută…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Caută" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Configurări" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Configurări — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Pagina actuală. Progres: %1 procente." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Navighează la %1. Progres: %2 procente." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Pagina actuală." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Navighează la %1. Cere atenție." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Navighează la %1." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Închide sertarul" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Deschide sertarul" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navighează înapoi" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navighează înainte" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Acțiuni suplimentare" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Sistemul de ferestre %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (construit cu %3)" diff --git a/po/ru/libkirigami2plugin_qt.po b/po/ru/libkirigami2plugin_qt.po new file mode 100644 index 0000000..abeb249 --- /dev/null +++ b/po/ru/libkirigami2plugin_qt.po @@ -0,0 +1,253 @@ +# Alexander Potashev , 2016, 2017, 2019. +# Alexander Yavorsky , 2019, 2020, 2021. +# Мария Шикунова , 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-02-16 15:37+0300\n" +"Last-Translator: Мария Шикунова \n" +"Language-Team: Russian \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 21.08.3\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Перейти на страницу %1 в магазине приложений KDE" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Send an email to %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Присоединиться к команде" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Авторские права" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Лицензия:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Лицензия: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Используемые библиотеки" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Авторы" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Показать фотографии авторов" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Благодарности" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Переводчики" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "О программе %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Сообщить об ошибке…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Выход" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Больше действий" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Удаление метки" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Действия" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Назад" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Закрыть боковую панель" + +#: controls/GlobalDrawer.qml:594 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Закрыть боковую панель" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Пароль" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Поиск…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Поиск" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Параметры" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Параметры — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Текущая страница. Ход выполнения: %1%." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Переход к %1. Ход выполнения: %2%." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Текущая страница." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Перейти к %1. Требует внимания." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Перейти к %1." + +#: controls/templates/OverlayDrawer.qml:140 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Закрыть боковую панель" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Перейти назад" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Перейти вперёд" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Больше действий" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Платформа графического сервера %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (собрана с версией %3)" diff --git a/po/sk/libkirigami2plugin_qt.po b/po/sk/libkirigami2plugin_qt.po new file mode 100644 index 0000000..23fe388 --- /dev/null +++ b/po/sk/libkirigami2plugin_qt.po @@ -0,0 +1,287 @@ +# 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:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:130 +#, fuzzy, qt-format +#| 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" + +#: controls/AboutItem.qml:139 +#, 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:186 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Get Involved" +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Zapojiť sa" + +#: controls/AboutItem.qml:198 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "License:" +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licencia:" + +#: controls/AboutItem.qml:269 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "License: %1" +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licencia: %1" + +#: controls/AboutItem.qml:280 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Libraries in use" +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Použité knižnice" + +#: controls/AboutItem.qml:310 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Authors" +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Autori" + +#: controls/AboutItem.qml:319 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Show author photos" +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Zobraziť fotografie autora" + +#: controls/AboutItem.qml:350 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Credits" +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Zásluhy" + +#: controls/AboutItem.qml:363 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Translators" +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Preklady" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "O %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Nahlásiť chybu..." + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Ukončiť" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Viac akcií" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Akcie" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Späť" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Zatvoriť bočný panel" + +#: controls/GlobalDrawer.qml:594 +#, 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:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Heslo" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Hľadať..." + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Hľadať" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Nastavenia" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, fuzzy, qt-format +#| msgctxt "CategorizedSettings|" +#| msgid "Settings" +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Nastavenia" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Aktuálna stránka. Postup: %1 percent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Navigovať do %1. Postup: %2 percent. " + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Aktuálna stránka." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Navigovať do %1. Vyžaduje si pozornosť." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Navigovať do %1." + +#: controls/templates/OverlayDrawer.qml:140 +#, fuzzy +#| msgctxt "GlobalDrawer|" +#| msgid "Close Sidebar" +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Zatvoriť bočný panel" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navigovať späť" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navigovať dopredu" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Viac akcií" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworky %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "The %1 systém okien" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (zostavené s %3)" diff --git a/po/sl/libkirigami2plugin_qt.po b/po/sl/libkirigami2plugin_qt.po new file mode 100644 index 0000000..fae5eb1 --- /dev/null +++ b/po/sl/libkirigami2plugin_qt.po @@ -0,0 +1,250 @@ +# 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: 2022-07-08 06:56+0200\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: Lokalize 22.04.1\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Obiščite stran %1 v KDE Store" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Pošljite e-pošto za %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Sodelujte" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Dovoljenje:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Dovoljenje: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Knjižnice v rabi" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Avtorji" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Prikaži fotografije avtorjev" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Zasluge" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Prevajalci" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "O programu %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Poročajte o napaki …" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Zapusti" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Več dejanj" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Odstrani značko" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Dejanja" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Nazaj" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Zapri stransko letvico" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Odpri stransko letvico" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Nalaganje…" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Geslo" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Poišči …" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Išči" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Nastavitve" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Nastavitve — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Trenutna stran. Napredek: %1 odst." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Krmari proti %1. Napredek: %2 odst." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Trenutna stran." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Krmari proti %1. Zahteva pozornost." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Krmari proti %1." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Zapri predal" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Odpri predal" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Krmari nazaj" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Krmari naprej" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Več dejanj" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Kopiraj povezavo na odložišče" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Sistem oken %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (zgrajeno za %3)" diff --git a/po/sr/libkirigami2plugin_qt.po b/po/sr/libkirigami2plugin_qt.po new file mode 100644 index 0000000..9a24d6c --- /dev/null +++ b/po/sr/libkirigami2plugin_qt.po @@ -0,0 +1,131 @@ +# 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/AboutPage.qml:66 +msgctxt "AboutPage|" +msgid "About" +msgstr "" + +#: controls/AboutPage.qml:103 +#, qt-format +msgctxt "AboutPage|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutPage.qml:152 +msgctxt "AboutPage|" +msgid "Copyright" +msgstr "" + +#: controls/AboutPage.qml:175 +msgctxt "AboutPage|" +msgid "License:" +msgstr "" + +#: controls/AboutPage.qml:188 +#, qt-format +msgctxt "AboutPage|" +msgid "License: %1" +msgstr "" + +#: controls/AboutPage.qml:199 +msgctxt "AboutPage|" +msgid "Libraries in use" +msgstr "" + +#: controls/AboutPage.qml:213 +msgctxt "AboutPage|" +msgid "Authors" +msgstr "" + +#: controls/AboutPage.qml:223 +msgctxt "AboutPage|" +msgid "Credits" +msgstr "" + +#: controls/AboutPage.qml:234 +msgctxt "AboutPage|" +msgid "Translators" +msgstr "" + +#: controls/ContextDrawer.qml:66 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Радње" + +#: controls/GlobalDrawer.qml:466 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Назад" + +#: controls/GlobalDrawer.qml:557 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "" + +#: controls/PasswordField.qml:32 +msgctxt "PasswordField|" +msgid "Password" +msgstr "" + +#: controls/SearchField.qml:31 +msgctxt "SearchField|" +msgid "Search..." +msgstr "" + +#: controls/templates/private/BackButton.qml:34 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Иди назад" + +#: controls/templates/private/ForwardButton.qml:30 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Иди напред" + +#: controls/ToolBarApplicationHeader.qml:113 +#, fuzzy +#| msgctxt "ContextDrawer|" +#| msgid "Actions" +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Радње" + +#: controls/UrlButton.qml:45 +msgctxt "UrlButton|" +msgid "Copy link address" +msgstr "" + +#: settings.cpp:197 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "" + +#: settings.cpp:199 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "" + +#: settings.cpp:200 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "" diff --git a/po/sr@ijekavian/libkirigami2plugin_qt.po b/po/sr@ijekavian/libkirigami2plugin_qt.po new file mode 100644 index 0000000..a12310f --- /dev/null +++ b/po/sr@ijekavian/libkirigami2plugin_qt.po @@ -0,0 +1,131 @@ +# 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/AboutPage.qml:66 +msgctxt "AboutPage|" +msgid "About" +msgstr "" + +#: controls/AboutPage.qml:103 +#, qt-format +msgctxt "AboutPage|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutPage.qml:152 +msgctxt "AboutPage|" +msgid "Copyright" +msgstr "" + +#: controls/AboutPage.qml:175 +msgctxt "AboutPage|" +msgid "License:" +msgstr "" + +#: controls/AboutPage.qml:188 +#, qt-format +msgctxt "AboutPage|" +msgid "License: %1" +msgstr "" + +#: controls/AboutPage.qml:199 +msgctxt "AboutPage|" +msgid "Libraries in use" +msgstr "" + +#: controls/AboutPage.qml:213 +msgctxt "AboutPage|" +msgid "Authors" +msgstr "" + +#: controls/AboutPage.qml:223 +msgctxt "AboutPage|" +msgid "Credits" +msgstr "" + +#: controls/AboutPage.qml:234 +msgctxt "AboutPage|" +msgid "Translators" +msgstr "" + +#: controls/ContextDrawer.qml:66 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Радње" + +#: controls/GlobalDrawer.qml:466 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Назад" + +#: controls/GlobalDrawer.qml:557 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "" + +#: controls/PasswordField.qml:32 +msgctxt "PasswordField|" +msgid "Password" +msgstr "" + +#: controls/SearchField.qml:31 +msgctxt "SearchField|" +msgid "Search..." +msgstr "" + +#: controls/templates/private/BackButton.qml:34 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Иди назад" + +#: controls/templates/private/ForwardButton.qml:30 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Иди напред" + +#: controls/ToolBarApplicationHeader.qml:113 +#, fuzzy +#| msgctxt "ContextDrawer|" +#| msgid "Actions" +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Радње" + +#: controls/UrlButton.qml:45 +msgctxt "UrlButton|" +msgid "Copy link address" +msgstr "" + +#: settings.cpp:197 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "" + +#: settings.cpp:199 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "" + +#: settings.cpp:200 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "" diff --git a/po/sr@ijekavianlatin/libkirigami2plugin_qt.po b/po/sr@ijekavianlatin/libkirigami2plugin_qt.po new file mode 100644 index 0000000..bd40cb7 --- /dev/null +++ b/po/sr@ijekavianlatin/libkirigami2plugin_qt.po @@ -0,0 +1,131 @@ +# 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/AboutPage.qml:66 +msgctxt "AboutPage|" +msgid "About" +msgstr "" + +#: controls/AboutPage.qml:103 +#, qt-format +msgctxt "AboutPage|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutPage.qml:152 +msgctxt "AboutPage|" +msgid "Copyright" +msgstr "" + +#: controls/AboutPage.qml:175 +msgctxt "AboutPage|" +msgid "License:" +msgstr "" + +#: controls/AboutPage.qml:188 +#, qt-format +msgctxt "AboutPage|" +msgid "License: %1" +msgstr "" + +#: controls/AboutPage.qml:199 +msgctxt "AboutPage|" +msgid "Libraries in use" +msgstr "" + +#: controls/AboutPage.qml:213 +msgctxt "AboutPage|" +msgid "Authors" +msgstr "" + +#: controls/AboutPage.qml:223 +msgctxt "AboutPage|" +msgid "Credits" +msgstr "" + +#: controls/AboutPage.qml:234 +msgctxt "AboutPage|" +msgid "Translators" +msgstr "" + +#: controls/ContextDrawer.qml:66 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Radnje" + +#: controls/GlobalDrawer.qml:466 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Nazad" + +#: controls/GlobalDrawer.qml:557 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "" + +#: controls/PasswordField.qml:32 +msgctxt "PasswordField|" +msgid "Password" +msgstr "" + +#: controls/SearchField.qml:31 +msgctxt "SearchField|" +msgid "Search..." +msgstr "" + +#: controls/templates/private/BackButton.qml:34 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Idi nazad" + +#: controls/templates/private/ForwardButton.qml:30 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Idi napred" + +#: controls/ToolBarApplicationHeader.qml:113 +#, fuzzy +#| msgctxt "ContextDrawer|" +#| msgid "Actions" +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Radnje" + +#: controls/UrlButton.qml:45 +msgctxt "UrlButton|" +msgid "Copy link address" +msgstr "" + +#: settings.cpp:197 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "" + +#: settings.cpp:199 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "" + +#: settings.cpp:200 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "" diff --git a/po/sr@latin/libkirigami2plugin_qt.po b/po/sr@latin/libkirigami2plugin_qt.po new file mode 100644 index 0000000..ac6ea34 --- /dev/null +++ b/po/sr@latin/libkirigami2plugin_qt.po @@ -0,0 +1,131 @@ +# 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/AboutPage.qml:66 +msgctxt "AboutPage|" +msgid "About" +msgstr "" + +#: controls/AboutPage.qml:103 +#, qt-format +msgctxt "AboutPage|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutPage.qml:152 +msgctxt "AboutPage|" +msgid "Copyright" +msgstr "" + +#: controls/AboutPage.qml:175 +msgctxt "AboutPage|" +msgid "License:" +msgstr "" + +#: controls/AboutPage.qml:188 +#, qt-format +msgctxt "AboutPage|" +msgid "License: %1" +msgstr "" + +#: controls/AboutPage.qml:199 +msgctxt "AboutPage|" +msgid "Libraries in use" +msgstr "" + +#: controls/AboutPage.qml:213 +msgctxt "AboutPage|" +msgid "Authors" +msgstr "" + +#: controls/AboutPage.qml:223 +msgctxt "AboutPage|" +msgid "Credits" +msgstr "" + +#: controls/AboutPage.qml:234 +msgctxt "AboutPage|" +msgid "Translators" +msgstr "" + +#: controls/ContextDrawer.qml:66 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Radnje" + +#: controls/GlobalDrawer.qml:466 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Nazad" + +#: controls/GlobalDrawer.qml:557 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "" + +#: controls/PasswordField.qml:32 +msgctxt "PasswordField|" +msgid "Password" +msgstr "" + +#: controls/SearchField.qml:31 +msgctxt "SearchField|" +msgid "Search..." +msgstr "" + +#: controls/templates/private/BackButton.qml:34 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Idi nazad" + +#: controls/templates/private/ForwardButton.qml:30 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Idi napred" + +#: controls/ToolBarApplicationHeader.qml:113 +#, fuzzy +#| msgctxt "ContextDrawer|" +#| msgid "Actions" +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Radnje" + +#: controls/UrlButton.qml:45 +msgctxt "UrlButton|" +msgid "Copy link address" +msgstr "" + +#: settings.cpp:197 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "" + +#: settings.cpp:199 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "" + +#: settings.cpp:200 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "" diff --git a/po/sv/libkirigami2plugin_qt.po b/po/sv/libkirigami2plugin_qt.po new file mode 100644 index 0000000..b9a0971 --- /dev/null +++ b/po/sv/libkirigami2plugin_qt.po @@ -0,0 +1,244 @@ +# Stefan Asserhäll , 2016, 2017, 2018, 2019, 2020, 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-08-03 10:08+0200\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 20.08.1\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Besök KDE-butikens sida för %1" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Skicka e-post till %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Engagera dig" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Licens:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Licens: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Använda bibliotek" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Upphovsmän" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Visa foton av upphovsmän" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Tack till" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Översättare" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Om %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Rapportera fel…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Avsluta" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Fler åtgärder" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Ta bort etikett" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Åtgärder" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Tillbaka" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Stäng sidorad" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Öppna sidorad" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Läser in…" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Lösenord" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Sök…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Sök" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Inställningar" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Inställningar — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Aktuell sida. Förlopp: %1 procent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Navigera till %1. Förlopp: %2 procent." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Aktuell sida." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Navigera till %1. Kräver uppmärksamhet." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Navigera till %1." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Stäng låda" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Öppna låda" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Navigera bakåt" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Navigera framåt" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Fler åtgärder" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Kopiera länk till klippbordet" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Ramverk %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Fönsterhanteringssystemet %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (byggt för %3)" diff --git a/po/ta/libkirigami2plugin_qt.po b/po/ta/libkirigami2plugin_qt.po new file mode 100644 index 0000000..2ad54db --- /dev/null +++ b/po/ta/libkirigami2plugin_qt.po @@ -0,0 +1,244 @@ +# Kishore G , 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-07-12 21:39+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 22.04.3\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "%1-இன் கே.டீ.யீ. கடைவீதி பக்கத்தை பாருங்கள்" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "%1 என்பவருக்கு மின்னஞ்சல் அனுப்பு" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "பங்களித்தல்" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "பதிப்புரிமை" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "உரிமம்:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "உரிமம்: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "பயன்படுத்தும் நிரலகங்கள்" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "இயற்றியவர்கள்" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "இயற்றியவர்களின் படங்களைக் காட்டு" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "நன்றி" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "மொழிபெயர்ப்பாளர்கள்" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1 பற்றி " + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "பிழையை தெரிவி…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "வெளியேறு" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "மேலும் செயல்கள்" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "குறிச்சொல்லை நீக்கு" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "செயல்கள்" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "பின்னே" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "ஓரப்பட்டையை மூடு" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "ஓரப்பட்டையை திற" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "ஏற்றப்படுகிறது…" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "கடவுச்சொல்" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "தேடு..." + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "தேடல்" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "அமைப்புகள்" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "அமைப்புகள் — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "தற்போதைய பக்கம். முன்னேற்றம்: %1 சதவீதம்." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "%1-க்கு செல். முன்னேற்றம்: %2 சதவீதம்." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "தற்போதைய பக்கம்" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "%1-க்கு செல். கவனத்தை கோருகிறது." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "%1-க்கு செல்." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "மேல்தோன்றும் பலகையை மூடு" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "மேல்தோன்றும் பலகையைத் திற" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "பின்னே செல்" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "முன்னே செல்" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "மேலும் செயல்கள்" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "இணைப்பை பிடிப்புப்பலகைக்கு நகலெடு" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "கே.டீ.யீ. நிரலகங்கள் %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "%1 சாளர நெறிமுறை" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (%3 கொண்டு தொகுக்கப்பட்டது)" diff --git a/po/tg/libkirigami2plugin_qt.po b/po/tg/libkirigami2plugin_qt.po new file mode 100644 index 0000000..2c6200a --- /dev/null +++ b/po/tg/libkirigami2plugin_qt.po @@ -0,0 +1,277 @@ +# 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:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "" + +#: controls/AboutItem.qml:198 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Copyright" +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Ҳуқуқи муаллиф" + +#: controls/AboutItem.qml:247 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "License:" +msgctxt "AboutItem|" +msgid "License:" +msgstr "Иҷозатнома:" + +#: controls/AboutItem.qml:269 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "License: %1" +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Иҷозатнома: %1" + +#: controls/AboutItem.qml:280 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Libraries in use" +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Китобхонаҳое, ки истифода мешаванд" + +#: controls/AboutItem.qml:310 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Authors" +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Муаллифон" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "" + +#: controls/AboutItem.qml:350 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Credits" +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Сипосгузорӣ" + +#: controls/AboutItem.qml:363 +#, fuzzy +#| msgctxt "AboutPage|" +#| msgid "Translators" +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Тарҷумонон" + +#: controls/AboutPage.qml:85 +#, fuzzy, qt-format +#| msgctxt "AboutPage|" +#| msgid "About" +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Дар бораи барнома" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "" + +#: controls/ActionToolBar.qml:204 +#, fuzzy +#| msgctxt "ToolBarApplicationHeader|" +#| msgid "More Actions" +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Амалҳои бештар" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Амалҳо" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Ба қафо" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Ниҳонвожа" + +#: controls/SearchField.qml:95 +#, fuzzy +#| msgctxt "SearchField|" +#| msgid "Search..." +msgctxt "SearchField|" +msgid "Search…" +msgstr "Ҷустуҷӯ..." + +#: controls/SearchField.qml:97 +#, fuzzy +#| msgctxt "SearchField|" +#| msgid "Search..." +msgctxt "SearchField|" +msgid "Search" +msgstr "Ҷустуҷӯ..." + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, fuzzy, qt-format +#| msgctxt "ForwardButton|" +#| msgid "Navigate Forward" +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Ба пеш паймудан" + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Ба қафо паймудан" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Ба пеш паймудан" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Амалҳои бештар" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "" diff --git a/po/tr/libkirigami2plugin_qt.po b/po/tr/libkirigami2plugin_qt.po new file mode 100644 index 0000000..17f5610 --- /dev/null +++ b/po/tr/libkirigami2plugin_qt.po @@ -0,0 +1,246 @@ +# Volkan Gezer , 2021. +# Emir SARI , 2022. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2022-07-07 11:19+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 22.04.2\n" +"X-Qt-Contexts: true\n" +"X-POOTLE-MTIME: 1497529432.000000\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "%1 KDE Mağaza sayfasını ziyaret et" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "%1 kişisine bir e-posta gönder" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Katıl" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Telif hakkı" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Lisans:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Lisans: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Kullanılan kitaplıklar" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Yazarlar" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Yazar fotoğraflarını göster" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Teşekkürler" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Çevirmenler" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "%1 Hakkında" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Hata Bildir…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Çık" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Daha Fazla Eylem" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Etiketi Kaldır" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Eylemler" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Geri" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Kenar Çubuğunu Kapat" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Kenar Çubuğunu Aç" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Yükleniyor…" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Parola" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Ara…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Ara" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Ayarlar" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Ayarlar — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Geçerli sayfa. İlerleme: %​%1." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "%1 konumuna geç. İlerleme: %​%2." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Geçerli sayfa." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "%1 konumuna geç. İlgi bekleniyor." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "%1 konumuna geç." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Çekmeceyi kapat" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Çekmeceyi aç" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Geriye Git" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "İleri Git" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Daha Fazla Eylem" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Bağlantıyı Panoya Kopyala" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "%1 pencereleme sistemi" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (%3 üzerine yapılı)" diff --git a/po/uk/libkirigami2plugin_qt.po b/po/uk/libkirigami2plugin_qt.po new file mode 100644 index 0000000..62a6ab1 --- /dev/null +++ b/po/uk/libkirigami2plugin_qt.po @@ -0,0 +1,251 @@ +# 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. +msgid "" +msgstr "" +"Project-Id-Version: libkirigami2plugin_qt\n" +"PO-Revision-Date: 2022-07-07 09:16+0300\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 20.12.0\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "Відвідайте сторінку %1 у KDE Store" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "Надіслати повідомлення ел. пошти до %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "Участь у команді" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Авторські права" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "Ліцензування:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "Ліцензування: %1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "Використані бібліотеки" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "Автори" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "Показати фотографії авторів" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "Подяки" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "Перекладачі" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "Про %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "Повідомити про ваду…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "Вийти" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "Інші дії" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "Вилучити мітку" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "Дії" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "Назад" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "Закрити бічну панель" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "Відкрити бічну панель" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "Завантаження…" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "Пароль" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "Шукати…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "Пошук" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "Параметри" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "Параметри — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "Поточна сторінка. Поступ: %1 відсотків." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "Перехід до %1. Поступ: %2 відсотків." + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "Поточна сторінка." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "Перейти до %1. Вимагає уваги." + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "Перейти до «%1»." + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "Закрити висувну панель" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "Відкрити висувну панель" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "Перейти назад" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "Перейти далі" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "Інші дії" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "Копіювати посилання до буфера" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "Система керування вікнами %1" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (зібрано з використанням %3)" diff --git a/po/zh_CN/libkirigami2plugin_qt.po b/po/zh_CN/libkirigami2plugin_qt.po new file mode 100644 index 0000000..6576b03 --- /dev/null +++ b/po/zh_CN/libkirigami2plugin_qt.po @@ -0,0 +1,246 @@ +msgid "" +msgstr "" +"Project-Id-Version: kdeorg\n" +"PO-Revision-Date: 2022-08-20 14:17\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: /kf5-trunk/messages/kirigami/libkirigami2plugin_qt.pot\n" +"X-Crowdin-File-ID: 6217\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1 (%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "访问 %1 的 KDE 商店页面" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "发送电子邮件到 %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "参与工作" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "Copyright" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "许可证:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "许可证:%1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "使用的程序库" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "开发人员" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "显示开发人员照片" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "致谢" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "翻译人员" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "关于 %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "报告程序缺陷…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "退出" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "更多操作" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "移除标签" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "操作" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "返回" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "关闭侧栏" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "打开侧边栏" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "正在加载…" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "密码" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "搜索…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "搜索" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "设置" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "设置 — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "当前页面。进度:百分之 %1。" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "前往 %1。进度:百分之 %2。" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "当前页面。" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "前往 %1。要求关注。" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "前往 %1。" + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "关闭抽屉栏" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "打开抽屉栏" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "后退" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "前进" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "更多操作" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "复制链接至剪贴板" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE 程序框架 %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "%1 窗口系统" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (使用 %3 构建)" diff --git a/po/zh_TW/libkirigami2plugin_qt.po b/po/zh_TW/libkirigami2plugin_qt.po new file mode 100644 index 0000000..3975b41 --- /dev/null +++ b/po/zh_TW/libkirigami2plugin_qt.po @@ -0,0 +1,249 @@ +# Jeff Huang , 2016. +# Franklin Weng , 2017. +# pan93412 , 2018, 2019. +# Kisaragi Hiu , 2022. +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: 2022-07-23 10:39+0800\n" +"Last-Translator: Yi-Jyun Pan \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: Poedit 3.1.1\n" +"X-Qt-Contexts: true\n" + +#: controls/AboutItem.qml:121 +#, qt-format +msgctxt "AboutItem|" +msgid "%1 (%2)" +msgstr "%1(%2)" + +#: controls/AboutItem.qml:130 +#, qt-format +msgctxt "AboutItem|" +msgid "Visit %1's KDE Store page" +msgstr "造訪 %1 在 KDE Store 的頁面" + +#: controls/AboutItem.qml:139 +#, qt-format +msgctxt "AboutItem|" +msgid "Send an email to %1" +msgstr "傳送電子郵件給 %1" + +#: controls/AboutItem.qml:186 +msgctxt "AboutItem|" +msgid "Get Involved" +msgstr "參與" + +#: controls/AboutItem.qml:198 +msgctxt "AboutItem|" +msgid "Copyright" +msgstr "著作權" + +#: controls/AboutItem.qml:247 +msgctxt "AboutItem|" +msgid "License:" +msgstr "授權條款:" + +#: controls/AboutItem.qml:269 +#, qt-format +msgctxt "AboutItem|" +msgid "License: %1" +msgstr "授權條款:%1" + +#: controls/AboutItem.qml:280 +msgctxt "AboutItem|" +msgid "Libraries in use" +msgstr "使用函式庫" + +#: controls/AboutItem.qml:310 +msgctxt "AboutItem|" +msgid "Authors" +msgstr "作者群" + +#: controls/AboutItem.qml:319 +msgctxt "AboutItem|" +msgid "Show author photos" +msgstr "顯示作者照片" + +#: controls/AboutItem.qml:350 +msgctxt "AboutItem|" +msgid "Credits" +msgstr "致謝" + +#: controls/AboutItem.qml:363 +msgctxt "AboutItem|" +msgid "Translators" +msgstr "翻譯者" + +#: controls/AboutPage.qml:85 +#, qt-format +msgctxt "AboutPage|" +msgid "About %1" +msgstr "關於 %1" + +#: controls/AboutPage.qml:88 +msgctxt "AboutPage|" +msgid "Report Bug…" +msgstr "回報臭蟲…" + +#: controls/AbstractApplicationWindow.qml:344 +msgctxt "AbstractApplicationWindow|" +msgid "Quit" +msgstr "離開" + +#: controls/ActionToolBar.qml:204 +msgctxt "ActionToolBar|" +msgid "More Actions" +msgstr "更多動作" + +#: controls/Avatar.qml:161 +#, qt-format +msgctxt "Avatar|" +msgid "%1 — %2" +msgstr "%1 — %2" + +#: controls/Chip.qml:82 +msgctxt "Chip|" +msgid "Remove Tag" +msgstr "移除標籤" + +#: controls/ContextDrawer.qml:69 +msgctxt "ContextDrawer|" +msgid "Actions" +msgstr "動作" + +#: controls/GlobalDrawer.qml:498 +msgctxt "GlobalDrawer|" +msgid "Back" +msgstr "返回" + +#: controls/GlobalDrawer.qml:591 +msgctxt "GlobalDrawer|" +msgid "Close Sidebar" +msgstr "關閉側邊欄" + +#: controls/GlobalDrawer.qml:594 +msgctxt "GlobalDrawer|" +msgid "Open Sidebar" +msgstr "開啟側邊欄" + +#: controls/LoadingPlaceholder.qml:54 +msgctxt "LoadingPlaceholder|" +msgid "Loading…" +msgstr "載入中……" + +#: controls/PasswordField.qml:43 +msgctxt "PasswordField|" +msgid "Password" +msgstr "密碼" + +#: controls/SearchField.qml:95 +msgctxt "SearchField|" +msgid "Search…" +msgstr "搜尋…" + +#: controls/SearchField.qml:97 +msgctxt "SearchField|" +msgid "Search" +msgstr "搜尋" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#: controls/settingscomponents/CategorizedSettings.qml:53 +msgctxt "CategorizedSettings|" +msgid "Settings" +msgstr "設定" + +#: controls/settingscomponents/CategorizedSettings.qml:30 +#, qt-format +msgctxt "CategorizedSettings|" +msgid "Settings — %1" +msgstr "設定 — %1" + +#. Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. +#: controls/swipenavigator/templates/PageTab.qml:39 +#, qt-format +msgctxt "PageTab|" +msgid "Current page. Progress: %1 percent." +msgstr "目前頁面。進度:百分之 %1。" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:42 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Progress: %2 percent." +msgstr "前往 %1。進度:百分之 %2。" + +#. Accessibility text for a page tab. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:47 +msgctxt "PageTab|" +msgid "Current page." +msgstr "目前頁面。" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:50 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1. Demanding attention." +msgstr "前往 %1。正在請求注意。" + +#. Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. +#: controls/swipenavigator/templates/PageTab.qml:53 +#, qt-format +msgctxt "PageTab|" +msgid "Navigate to %1." +msgstr "前往 %1。" + +#: controls/templates/OverlayDrawer.qml:140 +msgctxt "OverlayDrawer|" +msgid "Close drawer" +msgstr "關閉抽屜" + +#: controls/templates/OverlayDrawer.qml:146 +msgctxt "OverlayDrawer|" +msgid "Open drawer" +msgstr "開啟抽屜" + +#: controls/templates/private/BackButton.qml:36 +msgctxt "BackButton|" +msgid "Navigate Back" +msgstr "返回" + +#: controls/templates/private/ForwardButton.qml:34 +msgctxt "ForwardButton|" +msgid "Navigate Forward" +msgstr "往前" + +#: controls/ToolBarApplicationHeader.qml:114 +msgctxt "ToolBarApplicationHeader|" +msgid "More Actions" +msgstr "更多動作" + +#: controls/UrlButton.qml:47 +msgctxt "UrlButton|" +msgid "Copy Link to Clipboard" +msgstr "複製連結至剪貼簿" + +#: settings.cpp:220 +#, qt-format +msgctxt "Settings|" +msgid "KDE Frameworks %1" +msgstr "KDE Frameworks %1" + +#: settings.cpp:222 +#, qt-format +msgctxt "Settings|" +msgid "The %1 windowing system" +msgstr "%1 視窗系統" + +#: settings.cpp:223 +#, qt-format +msgctxt "Settings|" +msgid "Qt %2 (built against %3)" +msgstr "Qt %2 (建置於 %3 上)" 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..bbee9f7 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,310 @@ + +add_subdirectory(libkirigami) + +ecm_add_qml_module(KirigamiPlugin URI "org.kde.kirigami" VERSION 2.0 CLASSNAME KirigamiPlugin) + +ecm_add_qml_module_dependencies(KirigamiPlugin DEPENDS + "QtQuick.Controls 2.15" + "QtGraphicalEffects 1.0" +) + +ecm_create_qm_loader(kirigami_QM_LOADER libkirigami2plugin_qt) +target_sources(KirigamiPlugin PRIVATE ${kirigami_QM_LOADER}) + +target_include_directories(KirigamiPlugin PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/libkirigami + ${CMAKE_CURRENT_BINARY_DIR}/libkirigami +) + +target_sources(KirigamiPlugin PRIVATE + avatar.cpp + avatar.h + colorutils.cpp + colorutils.h + columnview.cpp + columnview.h + columnview_p.h + delegaterecycler.cpp + delegaterecycler.h + enums.cpp + enums.h + formlayoutattached.cpp + formlayoutattached.h + icon.cpp + icon.h + imagecolors.cpp + imagecolors.h + kirigamiplugin.cpp + kirigamiplugin.h + mnemonicattached.cpp + mnemonicattached.h + pagepool.cpp + pagepool.h + pagerouter.cpp + pagerouter.h + scenepositionattached.cpp + scenepositionattached.h + settings.cpp + settings.h + shadowedrectangle.cpp + shadowedrectangle.h + shadowedtexture.cpp + shadowedtexture.h + sizegroup.cpp + sizegroup.h + spellcheckinghint.cpp + spellcheckinghint.h + toolbarlayout.cpp + toolbarlayoutdelegate.cpp + toolbarlayoutdelegate.h + toolbarlayout.h + wheelhandler.cpp + wheelhandler.h + inputmethod.cpp + + scenegraph/managedtexturenode.cpp + scenegraph/managedtexturenode.h + scenegraph/paintedrectangleitem.cpp + scenegraph/paintedrectangleitem.h + ${CMAKE_CURRENT_BINARY_DIR}/libkirigami/loggingcategory.cpp + + scenegraph/shaders/shaders.qrc +) +if (HAVE_QTGUI_OPENGL AND NOT KF6_PORTING_TODO) + target_sources(KirigamiPlugin PRIVATE + 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 + ) +endif() + +ecm_target_qml_sources(KirigamiPlugin SOURCES + controls/Action.qml + controls/AbstractApplicationHeader.qml + controls/AbstractApplicationWindow.qml + controls/AbstractListItem.qml + controls/ApplicationHeader.qml + controls/ToolBarApplicationHeader.qml + controls/ApplicationWindow.qml + controls/BasicListItem.qml + controls/OverlayDrawer.qml + controls/ContextDrawer.qml + controls/GlobalDrawer.qml + controls/Heading.qml + controls/Separator.qml + controls/PageRow.qml + controls/Label.qml + controls/OverlaySheet.qml + controls/Page.qml + controls/ScrollablePage.qml + controls/SwipeListItem.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.1 SOURCES + controls/AbstractItemViewHeader.qml + controls/ItemViewHeader.qml + controls/AbstractApplicationItem.qml + controls/ApplicationItem.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.3 SOURCES + controls/FormLayout.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.4 SOURCES + controls/AbstractCard.qml + controls/Card.qml + controls/CardsListView.qml + controls/CardsGridView.qml + controls/CardsLayout.qml + controls/InlineMessage.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.5 SOURCES + controls/ListItemDragHandle.qml + controls/ActionToolBar.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.6 SOURCES + controls/AboutPage.qml + controls/LinkButton.qml + controls/UrlButton.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.7 SOURCES + controls/ActionTextField.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.8 SOURCES + controls/SearchField.qml + controls/PasswordField.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.10 SOURCES + controls/ListSectionHeader.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.11 SOURCES + controls/PagePoolAction.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.12 SOURCES + controls/ShadowedImage.qml + controls/PlaceholderMessage.qml + controls/RouterWindow.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.13 SOURCES + controls/Avatar.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.13 PATH swipenavigator SOURCES + controls/swipenavigator/SwipeNavigator.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.14 SOURCES + controls/FlexColumn.qml + controls/CheckableListItem.qml + controls/Hero.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.17 PATH swipenavigator SOURCES + controls/swipenavigator/TabViewLayout.qml + controls/swipenavigator/PageTab.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.18 PATH settingscomponents SOURCES + controls/settingscomponents/CategorizedSettings.qml + controls/settingscomponents/SettingAction.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.19 SOURCES + controls/AboutItem.qml + controls/NavigationTabBar.qml + controls/NavigationTabButton.qml + controls/Dialog.qml + controls/MenuDialog.qml + controls/PromptDialog.qml + controls/AbstractChip.qml + controls/Chip.qml + controls/LoadingPlaceholder.qml +) + +ecm_target_qml_sources(KirigamiPlugin VERSION 2.20 SOURCES + controls/SelectableLabel.qml +) + +ecm_target_qml_sources(KirigamiPlugin PRIVATE PATH private SOURCES + controls/private/ActionButton.qml + controls/private/ActionIconGroup.qml + controls/private/ActionMenuItem.qml + controls/private/ActionsMenu.qml + controls/private/BannerImage.qml + controls/private/CardsGridViewPrivate.qml + controls/private/ContextDrawerActionItem.qml + controls/private/CornerShadow.qml + controls/private/DefaultCardBackground.qml + controls/private/DefaultChipBackground.qml + controls/private/DefaultListItemBackground.qml + controls/private/EdgeShadow.qml + controls/private/GlobalDrawerActionItem.qml + controls/private/PageActionPropertyGroup.qml + controls/private/PrivateActionToolButton.qml + controls/private/SwipeItemEventFilter.qml +) + +ecm_target_qml_sources(KirigamiPlugin 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/TabBarControl.qml + controls/private/globaltoolbar/TitlesPageHeader.qml + controls/private/globaltoolbar/ToolBarPageHeader.qml +) + +ecm_target_qml_sources(KirigamiPlugin PRIVATE PATH swipenavigator SOURCES + controls/swipenavigator/PrivateSwipeHighlight.qml + controls/swipenavigator/PrivateSwipeProgress.qml + controls/swipenavigator/PrivateSwipeStack.qml + controls/swipenavigator/PrivateSwipeTab.qml + controls/swipenavigator/PrivateSwipeTabBar.qml +) + +ecm_target_qml_sources(KirigamiPlugin PRIVATE PATH swipenavigator/templates SOURCES + controls/swipenavigator/templates/PageTab.qml +) + +ecm_target_qml_sources(KirigamiPlugin PRIVATE PATH templates SOURCES + controls/templates/AbstractApplicationHeader.qml + controls/templates/AbstractCard.qml + controls/templates/AbstractChip.qml + controls/templates/AbstractListItem.qml + controls/templates/ApplicationHeader.qml + controls/templates/InlineMessage.qml + controls/templates/OverlayDrawer.qml + controls/templates/OverlaySheet.qml + controls/templates/SingletonHeaderSizeGroup.qml + controls/templates/SwipeListItem.qml + controls/templates/qmldir +) + +ecm_target_qml_sources(KirigamiPlugin PRIVATE PATH templates/private SOURCES + controls/templates/private/BackButton.qml + controls/templates/private/BorderPropertiesGroup.qml + controls/templates/private/ContextIcon.qml + controls/templates/private/ForwardButton.qml + controls/templates/private/GenericDrawerIcon.qml + controls/templates/private/IconPropertiesGroup.qml + controls/templates/private/MenuIcon.qml + controls/templates/private/PassiveNotification.qml +) + +ecm_target_qml_sources(KirigamiPlugin PRIVATE PATH styles/Material SOURCES + styles/Material/AbstractListItem.qml + styles/Material/InlineMessage.qml + styles/Material/Label.qml + styles/Material/SwipeListItem.qml + styles/Material/Theme.qml +) + +if (DESKTOP_ENABLED) + ecm_target_qml_sources(KirigamiPlugin PRIVATE PATH styles/org.kde.desktop SOURCES + styles/org.kde.desktop/AbstractApplicationHeader.qml + styles/org.kde.desktop/AbstractListItem.qml + styles/org.kde.desktop/SwipeListItem.qml + styles/org.kde.desktop/Theme.qml + ) +endif() + +target_link_libraries(KirigamiPlugin + PUBLIC Qt${QT_MAJOR_VERSION}::Core + PRIVATE + ${Kirigami_EXTRA_LIBS} + Qt${QT_MAJOR_VERSION}::GuiPrivate + Qt${QT_MAJOR_VERSION}::Qml + Qt${QT_MAJOR_VERSION}::Quick + Qt${QT_MAJOR_VERSION}::QuickControls2 + Qt${QT_MAJOR_VERSION}::Concurrent + KF5Kirigami2 +) + +ecm_finalize_qml_module(KirigamiPlugin DESTINATION ${KDE_INSTALL_QMLDIR}) + +ecm_generate_qmltypes(org.kde.kirigami 2.0 DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami.2) + +if (ANDROID) + install(FILES KF5Kirigami2-android-dependencies.xml + DESTINATION ${KDE_INSTALL_LIBDIR} + RENAME KF5Kirigami2_${CMAKE_ANDROID_ARCH_ABI}-android-dependencies.xml + ) +endif() diff --git a/src/KF5Kirigami2-android-dependencies.xml b/src/KF5Kirigami2-android-dependencies.xml new file mode 100644 index 0000000..c494299 --- /dev/null +++ b/src/KF5Kirigami2-android-dependencies.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/Messages.sh b/src/Messages.sh new file mode 100644 index 0000000..1c52975 --- /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/libkirigami2plugin_qt.pot +rm -f rc.cpp + diff --git a/src/avatar.cpp b/src/avatar.cpp new file mode 100644 index 0000000..05a99eb --- /dev/null +++ b/src/avatar.cpp @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2020 Carson Black +// +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "avatar.h" +#include +#include +#include +#include +#include + +bool contains(const QString &str, QChar::Script s) +{ + for (auto rune : str) { + if (rune.script() == s) { + return true; + } + } + return false; +} + +QString NameUtils::initialsFromString(const QString &string) +{ + // "" -> "" + if (string.isEmpty()) { + return {}; + } + + QString normalized = string.normalized(QString::NormalizationForm_D); + + if (normalized.startsWith(QLatin1Char('#')) || normalized.startsWith(QLatin1Char('@'))) { + normalized.remove(0, 1); + } + + // Names written with Han and Hangul characters generally can be initialised by taking the + // first character + if (contains(normalized, QChar::Script_Han) || contains(normalized, QChar::Script_Hangul)) { + return normalized.at(0); + } + + // "FirstName Name Name LastName" + normalized = normalized.trimmed(); + if (normalized.contains(QLatin1Char(' '))) { + // "FirstName Name Name LastName" -> "FirstName" "Name" "Name" "LastName" +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + const auto split = QStringView(normalized).split(QLatin1Char(' ')); +#else + const auto split = normalized.splitRef(QLatin1Char(' ')); +#endif + + // "FirstName" + auto first = split.first(); + // "LastName" + auto last = split.last(); + if (first.isEmpty()) { + // "" "LastName" -> "L" + return QString(last.front()); + } + if (last.isEmpty()) { + // "FirstName" "" -> "F" + return QString(first.front()); + } + // "FirstName" "LastName" -> "FL" + return QString(first.front()) + last.front(); + // "OneName" + } else { + // "OneName" -> "O" + return QString(normalized.front()); + } +} + +/* clang-format off */ +const QMap> c_colors = { + { + QStringLiteral("default"), + { + QColor("#e93a9a"), + QColor("#e93d58"), + QColor("#e9643a"), + QColor("#ef973c"), + QColor("#e8cb2d"), + QColor("#b6e521"), + QColor("#3dd425"), + QColor("#00d485"), + QColor("#00d3b8"), + QColor("#3daee9"), + QColor("#b875dc"), + QColor("#926ee4"), + } + }, + { + QStringLiteral("Material"), + { + QColor("#f44336"), + QColor("#e91e63"), + QColor("#9c27b0"), + QColor("#673ab7"), + QColor("#3f51b5"), + QColor("#2196f3"), + QColor("#03a9f4"), + QColor("#00bcd4"), + QColor("#009688"), + QColor("#4caf50"), + QColor("#8bc34a"), + QColor("#cddc39"), + QColor("#ffeb3b"), + QColor("#ffc107"), + QColor("#ff9800"), + QColor("#ff5722"), + } + } +}; +/* clang-format on */ + +QList grabColors() +{ + if (c_colors.contains(QQuickStyle::name())) { + return c_colors[QQuickStyle::name()]; + } + return c_colors[QStringLiteral("default")]; +} + +auto NameUtils::colorsFromString(const QString &string) -> QColor +{ + // We use a hash to get a "random" number that's always the same for + // a given string. + auto hash = qHash(string); + // hash modulo the length of the colors list minus one will always get us a valid + // index + auto index = hash % (grabColors().length() - 1); + // return a colour + return grabColors()[index]; +} + +auto NameUtils::isStringUnsuitableForInitials(const QString &string) -> bool +{ + if (string.isEmpty()) { + return true; + } + + bool isNumber; + string.toFloat(&isNumber); + if (isNumber) { + return true; + } + + const auto scripts = QList{QChar::Script_Common, QChar::Script_Inherited, QChar::Script_Latin, QChar::Script_Han, QChar::Script_Hangul}; + + for (auto character : string) { + if (!scripts.contains(character.script())) { + return true; + } + } + return false; +} diff --git a/src/avatar.h b/src/avatar.h new file mode 100644 index 0000000..7719543 --- /dev/null +++ b/src/avatar.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2020 Carson Black +// +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include +#include +#include + +class NameUtils : public QObject +{ + Q_OBJECT + +public: + Q_INVOKABLE QString initialsFromString(const QString &name); + Q_INVOKABLE QColor colorsFromString(const QString &name); + Q_INVOKABLE bool isStringUnsuitableForInitials(const QString &name); +}; + +class AvatarGroup : public QObject +{ + Q_OBJECT + +public: + Q_PROPERTY(QVariant main MEMBER mainAction NOTIFY mainActionChanged) + QVariant mainAction; + Q_SIGNAL void mainActionChanged(); + + Q_PROPERTY(QVariant secondary MEMBER secondaryAction NOTIFY secondaryActionChanged) + QVariant secondaryAction; + Q_SIGNAL void secondaryActionChanged(); +}; diff --git a/src/colorutils.cpp b/src/colorutils.cpp new file mode 100644 index 0000000..2ab2201 --- /dev/null +++ b/src/colorutils.cpp @@ -0,0 +1,313 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "colorutils.h" + +#include "loggingcategory.h" +#include +#include +#include +#include + +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 scaleAlpha = [](const QColor &color, double factor) { + return QColor::fromRgb(color.red(), color.green(), color.blue(), color.alpha() * factor); + }; + auto linearlyInterpolateDouble = [](double one, double two, double factor) { + return one + (two - one) * factor; + }; + + if (one == Qt::transparent) { + return scaleAlpha(two, balance); + } + if (two == Qt::transparent) { + return scaleAlpha(one, 1 - balance); + } + + return QColor::fromHsv(std::fmod(linearlyInterpolateDouble(one.hue(), two.hue(), balance), 360.0), + qBound(0.0, linearlyInterpolateDouble(one.saturation(), two.saturation(), balance), 255.0), + qBound(0.0, linearlyInterpolateDouble(one.value(), two.value(), balance), 255.0), + qBound(0.0, linearlyInterpolateDouble(one.alpha(), two.alpha(), balance), 255.0)); +} + +// 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("lightness"), 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(KirigamiLog) << "It is an error to have both RGB and HSL 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(KirigamiLog) << "Hue is out of bounds"; + } + if (qBound(-255.0, adjusts.red, 255.0) != adjusts.red) { + qCCritical(KirigamiLog) << "Red is out of bounds"; + } + if (qBound(-255.0, adjusts.green, 255.0) != adjusts.green) { + qCCritical(KirigamiLog) << "Green is out of bounds"; + } + if (qBound(-255.0, adjusts.blue, 255.0) != adjusts.blue) { + qCCritical(KirigamiLog) << "Green is out of bounds"; + } + if (qBound(-255.0, adjusts.saturation, 255.0) != adjusts.saturation) { + qCCritical(KirigamiLog) << "Saturation is out of bounds"; + } + if (qBound(-255.0, adjusts.value, 255.0) != adjusts.value) { + qCCritical(KirigamiLog) << "Value is out of bounds"; + } + if (qBound(-255.0, adjusts.alpha, 255.0) != adjusts.alpha) { + qCCritical(KirigamiLog) << "Alpha is out of bounds"; + } + + auto copy = color; + + if (adjusts.alpha) { + copy.setAlpha(adjusts.alpha); + } + + if (adjusts.red || adjusts.green || adjusts.blue) { + copy.setRed(copy.red() + adjusts.red); + copy.setGreen(copy.green() + adjusts.green); + copy.setBlue(copy.blue() + adjusts.blue); + } else if (adjusts.hue || adjusts.saturation || adjusts.value) { + copy.setHsl(std::fmod(copy.hue() + adjusts.hue, 360.0), // + copy.saturation() + adjusts.saturation, // + copy.value() + adjusts.value, + 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(KirigamiLog) << "Red is out of bounds"; + } + if (qBound(-100.0, adjusts.green, 100.00) != adjusts.green) { + qCCritical(KirigamiLog) << "Green is out of bounds"; + } + if (qBound(-100.0, adjusts.blue, 100.00) != adjusts.blue) { + qCCritical(KirigamiLog) << "Blue is out of bounds"; + } + if (qBound(-100.0, adjusts.saturation, 100.00) != adjusts.saturation) { + qCCritical(KirigamiLog) << "Saturation is out of bounds"; + } + if (qBound(-100.0, adjusts.value, 100.00) != adjusts.value) { + qCCritical(KirigamiLog) << "Value is out of bounds"; + } + if (qBound(-100.0, adjusts.alpha, 100.00) != adjusts.alpha) { + qCCritical(KirigamiLog) << "Alpha is out of bounds"; + } + + if (adjusts.hue != 0) { + qCCritical(KirigamiLog) << "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.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.setHsl(copy.hue(), + qBound(0.0, shiftToAverage(copy.saturation(), adjusts.saturation), 255.0), + qBound(0.0, shiftToAverage(copy.value(), adjusts.value), 255.0), + qBound(0.0, shiftToAverage(copy.alpha(), adjusts.alpha), 255.0)); + } + + 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::LabColor ColorUtils::colorToLab(const QColor &color) +{ + // http://wiki.nuaj.net/index.php/Color_Transforms#RGB_.E2.86.92_XYZ + // First: convert to XYZ + qreal r = color.redF(); + qreal g = color.greenF(); + qreal b = color.blueF(); + + // Apply gamma correction (i.e. conversion to linear-space) + if (r > 0.04045) { + r = pow((r + 0.055) / 1.055, 2.4); + } else { + r = r / 12.92; + } + + if (g > 0.04045) { + g = pow((g + 0.055) / 1.055, 2.4); + } else { + g = g / 12.92; + } + + if (b > 0.04045) { + b = pow((b + 0.055) / 1.055, 2.4); + } else { + b = b / 12.92; + } + + // Observer. = 2°, Illuminant = D65 + qreal x = r * 0.4124 + g * 0.3576 + b * 0.1805; + qreal y = r * 0.2126 + g * 0.7152 + b * 0.0722; + qreal z = r * 0.0193 + g * 0.1192 + b * 0.9505; + + // Second: convert from XYZ to L*a*b + x = x / 0.95047; // Observer= 2°, Illuminant= D65 + y = y / 1.0; + z = z / 1.08883; + + if (x > 0.008856) { + x = pow(x, 1.0 / 3.0); + } else { + x = (7.787 * x) + (16.0 / 116.0); + } + + if (y > 0.008856) { + y = pow(y, 1.0 / 3.0); + } else { + y = (7.787 * y) + (16.0 / 116.0); + } + + if (z > 0.008856) { + z = pow(z, 1.0 / 3.0); + } else { + z = (7.787 * z) + (16.0 / 116.0); + } + + LabColor labColor; + labColor.l = (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)); +} diff --git a/src/colorutils.h b/src/colorutils.h new file mode 100644 index 0000000..51a4814 --- /dev/null +++ b/src/colorutils.h @@ -0,0 +1,209 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include +#include + +/** + * Utilities for processing items to obtain colors and information useful for + * UIs that need to adjust to variable elements. + */ +class ColorUtils : public QObject +{ + Q_OBJECT +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 2.0 + * import org.kde.kirigami 2.12 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 2.0 + * import org.kde.kirigami 2.12 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 2.0 + * import org.kde.kirigami 2.12 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. + * + * @note `value` and `lightness` are aliases for the same value. + * + * @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 + * lightness: null, // Range: -255 to 255 + * alpha: null, // Range: -255 to 255 + * } + * @endcode + * + * @warning It is an error to adjust both RGB and HSL 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. + * + * @note `value` and `lightness` are aliases for the same value. + * + * @code{.js} + * { + * red: null + * green: null + * blue: null + * saturation: null + * lightness: null + * value: null + * alpha: null + * } + * @endcode + * + * @warning It is an error to scale both RGB and HSL 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 LabColor { + qreal l = 0; + qreal a = 0; + qreal b = 0; + }; + + // Not for QML, returns the comvertion from srgb of a QColor and Lab colorspace + static ColorUtils::LabColor colorToLab(const QColor &color); +}; diff --git a/src/columnview.cpp b/src/columnview.cpp new file mode 100644 index 0000000..09692dc --- /dev/null +++ b/src/columnview.cpp @@ -0,0 +1,1785 @@ +/* + * 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 "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 = new QQmlComponent(engine, this); + + /* clang-format off */ + component->setData(QByteArrayLiteral(R"( +import QtQuick 2.7 +import org.kde.kirigami 2.7 as Kirigami + +QtObject { + id: root + readonly property Kirigami.Units units: Kirigami.Units + + readonly property Component separator: Kirigami.Separator { + property Item column + + visible: column.Kirigami.ColumnView.view && column.Kirigami.ColumnView.view.contentX < column.x + anchors.top: column.top + anchors.bottom: column.bottom + } + + readonly property Component rightSeparator: Kirigami.Separator { + property Item column + + anchors.top: column.top + anchors.right: column.right + anchors.bottom: column.bottom + } +} +)"), QUrl(QStringLiteral("columnview.cpp"))); + /* clang-format on */ + + m_instance = component->create(); + // qCWarning(KirigamiLog)<errors(); + Q_ASSERT(m_instance); + m_instance->setParent(this); + + m_separatorComponent = m_instance->property("separator").value(); + Q_ASSERT(m_separatorComponent); + + m_rightSeparatorComponent = m_instance->property("rightSeparator").value(); + Q_ASSERT(m_rightSeparatorComponent); + + m_units = engine->singletonInstance(qmlTypeId("org.kde.kirigami", 2, 0, "Units")); + Q_ASSERT(m_units); + + connect(m_units, &Kirigami::Units::gridUnitChanged, this, &QmlComponentsPool::gridUnitChanged); + connect(m_units, &Kirigami::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(); +} + +///////// + +ContentItem::ContentItem(ColumnView *parent) + : QQuickItem(parent) + , m_view(parent) +{ + 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::InOutQuad)); + 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); +} + +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(), 0); + if (!firstItem) { + return; + } + QQuickItem *nextItem = childAt(firstItem->x() + firstItem->width() + 1, 0); + + // 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()) { + return qRound(qBound(m_columnWidth, (parentItem()->width() - attached->reservedSpace()), 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->isVisible()) { + if (attached->isPinned() && m_view->columnResizeMode() != ColumnView::SingleColumn) { + QQuickItem *sep = nullptr; + int sepWidth = 0; + if (m_view->separatorVisible()) { + sep = ensureRightSeparator(child); + sepWidth = (sep ? sep->width() : 0); + } + const qreal width = childWidth(child); + child->setSize(QSizeF(width + sepWidth, height())); + + child->setPosition(QPointF(qMin(qMax(-x(), partialWidth), -x() + m_view->width() - child->width() + sepWidth), 0.0)); + 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 { + child->setSize(QSizeF(childWidth(child), height())); + + auto it = m_rightSeparators.find(child); + if (it != m_rightSeparators.end()) { + it.value()->deleteLater(); + m_rightSeparators.erase(it); + } + child->setPosition(QPointF(partialWidth, 0.0)); + 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 = ensureRightSeparator(child); + sepWidth = (sep ? sep->width() : 0); + } + + child->setPosition(QPointF(qMin(qMax(-x(), partialWidth), -x() + m_view->width() - child->width() + sepWidth), 0.0)); + + 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); + } else { + attached->setInViewport(false); + } + } + + for (auto *item : std::as_const(m_visibleItems)) { + disconnect(item, &QObject::destroyed, this, nullptr); + } + const QQuickItem *oldFirstVisibleItem = m_visibleItems.isEmpty() ? nullptr : qobject_cast(m_visibleItems.first()); + const QQuickItem *oldLastVisibleItem = m_visibleItems.isEmpty() ? nullptr : qobject_cast(m_visibleItems.last()); + + if (newItems != m_visibleItems) { + m_visibleItems = newItems; + Q_EMIT m_view->visibleItemsChanged(); + if (!newItems.isEmpty() && m_visibleItems.first() != oldFirstVisibleItem) { + Q_EMIT m_view->firstVisibleItemChanged(); + } + if (!newItems.isEmpty() && m_visibleItems.last() != oldLastVisibleItem) { + Q_EMIT m_view->lastVisibleItemChanged(); + } + } +} + +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_separators.take(item); + if (separatorItem) { + separatorItem->deleteLater(); + } + separatorItem = m_rightSeparators.take(item); + if (separatorItem) { + separatorItem->deleteLater(); + } + + const int index = m_items.indexOf(item); + m_items.removeAll(item); + disconnect(item, &QObject::destroyed, 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::ensureSeparator(QQuickItem *item) +{ + QQuickItem *separatorItem = m_separators.value(item); + + if (!separatorItem) { + separatorItem = qobject_cast( + privateQmlComponentsPoolSelf->instance(qmlEngine(item))->m_separatorComponent->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_separatorComponent->completeCreate(); + m_separators[item] = separatorItem; + } + } + + return separatorItem; +} + +QQuickItem *ContentItem::ensureRightSeparator(QQuickItem *item) +{ + QQuickItem *separatorItem = m_rightSeparators.value(item); + + if (!separatorItem) { + separatorItem = qobject_cast( + QmlComponentsPoolSingleton::instance(qmlEngine(item))->m_rightSeparatorComponent->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_rightSeparatorComponent->completeCreate(); + m_rightSeparators[item] = separatorItem; + } + } + + return separatorItem; +} + +void ContentItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) +{ + 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()) { + ensureSeparator(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); +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +void ContentItem::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + updateVisibleItems(); + QQuickItem::geometryChanged(newGeometry, oldGeometry); +} +#else +void ContentItem::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + updateVisibleItems(); + QQuickItem::geometryChange(newGeometry, oldGeometry); +} +#endif + +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())); + } +} + +ColumnView::ColumnView(QQuickItem *parent) + : QQuickItem(parent) + , m_contentItem(nullptr) +{ + // NOTE: this is to *not* trigger itemChange + m_contentItem = new ContentItem(this); + 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; + 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::firstVisibleItem() const +{ + if (m_contentItem->m_visibleItems.isEmpty()) { + return nullptr; + } + + return qobject_cast(m_contentItem->m_visibleItems.first()); +} + +QQuickItem *ColumnView::lastVisibleItem() 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; + + if (visible) { + for (QQuickItem *item : std::as_const(m_contentItem->m_items)) { + QQuickItem *sep = m_contentItem->ensureSeparator(item); + if (sep) { + sep->setVisible(true); + } + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + if (attached->isPinned()) { + QQuickItem *sep = m_contentItem->ensureRightSeparator(item); + if (sep) { + sep->setVisible(true); + } + } + } + + } else { + for (QQuickItem *sep : std::as_const(m_contentItem->m_separators)) { + sep->setVisible(false); + } + for (QQuickItem *sep : std::as_const(m_contentItem->m_rightSeparators)) { + sep->setVisible(false); + } + } + + 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); + + item->forceActiveFocus(); + + // 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(KirigamiLog) << "Position" << pos << "passed to ColumnView::replaceItem is out of range."; + return; + } + + if (!item) { + qCWarning(KirigamiLog) << "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); + + 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(const QVariant &item) +{ + if (item.canConvert()) { + return removeItem(item.value()); + } else if (item.canConvert()) { + return removeItem(item.toInt()); + } else { + return nullptr; + } +} + +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(int pos) +{ + if (m_contentItem->m_items.isEmpty() // + || pos < 0 || pos >= m_contentItem->m_items.length()) { + return nullptr; + } + + return removeItem(m_contentItem->m_items[pos]); +} + +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()); + // if no item has been passed, just pop one + if (!item) { + break; + } + } + return removed; +} + +void ColumnView::clear() +{ + for (QQuickItem *item : std::as_const(m_contentItem->m_items)) { + 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); +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +void ColumnView::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +#else +void ColumnView::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +#endif +{ + m_contentItem->setY(m_topPadding); + m_contentItem->setHeight(newGeometry.height() - m_topPadding - m_bottomPadding); + m_contentItem->m_shouldAnimate = false; + polish(); + + m_contentItem->updateVisibleItems(); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QQuickItem::geometryChanged(newGeometry, oldGeometry); +#else + QQuickItem::geometryChange(newGeometry, oldGeometry); +#endif +} + +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 (candidateItem->parentItem() == m_contentItem) { + setCurrentIndex(m_contentItem->m_items.indexOf(candidateItem)); + } + + // 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->localPos()).x(); + m_oldMouseY = m_startMouseY = mapFromItem(item, me->localPos()).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; + } + + const QPointF pos = mapFromItem(item, me->localPos()); + + 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->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->localPos()).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; + break; + } + case QEvent::MouseButtonRelease: { + QMouseEvent *me = static_cast(event); + + if (!m_acceptsMouse && me->source() == Qt::MouseEventNotSynthesized) { + 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 (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(); + } + + if (item->property("preventStealing").toBool()) { + return false; + } + + event->accept(); + + // if a drag happened, don't pass the event + const bool block = keepMouseGrab(); + setKeepMouseGrab(false); + + me->setAccepted(block); + return block; + break; + } + 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->localPos().x(); + m_startMouseX = event->localPos().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->localPos().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->longDuration()); + 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); +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +int ColumnView::contentChildren_count(QQmlListProperty *prop) +#else +qsizetype ColumnView::contentChildren_count(QQmlListProperty *prop) +#endif +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return 0; + } + + return view->m_contentItem->m_items.count(); +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +QQuickItem *ColumnView::contentChildren_at(QQmlListProperty *prop, int index) +#else +QQuickItem *ColumnView::contentChildren_at(QQmlListProperty *prop, qsizetype index) +#endif +{ + 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); + } +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +int ColumnView::contentData_count(QQmlListProperty *prop) +#else +qsizetype ColumnView::contentData_count(QQmlListProperty *prop) +#endif +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return 0; + } + + return view->m_contentData.count(); +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +QObject *ColumnView::contentData_at(QQmlListProperty *prop, int index) +#else +QObject *ColumnView::contentData_at(QQmlListProperty *prop, qsizetype index) +#endif +{ + 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" diff --git a/src/columnview.h b/src/columnview.h new file mode 100644 index 0000000..fd135c3 --- /dev/null +++ b/src/columnview.h @@ -0,0 +1,492 @@ +/* + * 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) + Q_PROPERTY(bool accepted MEMBER accepted) +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) + + /** + * If true, the column will expand to take the whole viewport space minus reservedSpace + */ + Q_PROPERTY(bool fillWidth READ fillWidth WRITE setFillWidth NOTIFY fillWidthChanged) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * The view this column belongs to + */ + Q_PROPERTY(ColumnView *view READ view NOTIFY viewChanged) + + /** + * 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) + +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); + +Q_SIGNALS: + void indexChanged(); + void fillWidthChanged(); + void reservedSpaceChanged(); + void viewChanged(); + void preventStealingChanged(); + void pinnedChanged(); + void scrollIntention(ScrollIntentionEvent *event); + void inViewportChanged(); + +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; +}; + +/** + * 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 + + /** + * 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 + */ + Q_PROPERTY(ColumnResizeMode columnResizeMode READ columnResizeMode WRITE setColumnResizeMode NOTIFY columnResizeModeChanged) + + /** + * The width of all columns when columnResizeMode is FixedColumns + */ + Q_PROPERTY(qreal columnWidth READ columnWidth WRITE setColumnWidth NOTIFY columnWidthChanged) + + /** + * How many columns this view containsItem*/ + Q_PROPERTY(int count READ count NOTIFY countChanged) + + /** + * 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) + + /** + * The currently active column. The current column will also have keyboard focus + */ + Q_PROPERTY(QQuickItem *currentItem READ currentItem NOTIFY currentItemChanged) + + /** + * The main content item of this view: it's the parent of the column items + */ + Q_PROPERTY(QQuickItem *contentItem READ contentItem CONSTANT) + + /** + * The value of the horizontal scroll of the view, in pixels + */ + Q_PROPERTY(qreal contentX READ contentX WRITE setContentX NOTIFY contentXChanged) + + /** + * The compound width of all columns in the view + */ + Q_PROPERTY(qreal contentWidth READ contentWidth NOTIFY contentWidthChanged) + + /** + * The padding this will have at the top + */ + Q_PROPERTY(qreal topPadding READ topPadding WRITE setTopPadding NOTIFY topPaddingChanged) + + /** + * The padding this will have at the bottom + */ + Q_PROPERTY(qreal bottomPadding READ bottomPadding WRITE setBottomPadding NOTIFY bottomPaddingChanged) + + /** + * The duration for scrolling animations + */ + Q_PROPERTY(int scrollDuration READ scrollDuration WRITE setScrollDuration NOTIFY scrollDurationChanged) + + /** + * True if columns should be visually separated by a separator line + */ + Q_PROPERTY(bool separatorVisible READ separatorVisible WRITE setSeparatorVisible NOTIFY separatorVisibleChanged) + + /** + * 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) + + /** + * The first of visibleItems provided from convenience + */ + Q_PROPERTY(QQuickItem *firstVisibleItem READ firstVisibleItem NOTIFY firstVisibleItemChanged) + + /** + * The last of visibleItems provided from convenience + */ + Q_PROPERTY(QQuickItem *lastVisibleItem READ lastVisibleItem NOTIFY lastVisibleItemChanged) + + // 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) + + /** + * 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) + + /** + * True if it supports moving the contents by dragging + */ + Q_PROPERTY(bool interactive READ interactive WRITE setInteractive NOTIFY interactiveChanged) + + /** + * True if the contents can be dragged also with mouse besides touch + */ + Q_PROPERTY(bool acceptsMouse READ acceptsMouse WRITE setAcceptsMouse NOTIFY acceptsMouseChanged) + + // 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(); + + // NOTE: It's a QList as QML can't correctly build an Array out of QList + QList visibleItems() const; + QQuickItem *firstVisibleItem() const; + QQuickItem *lastVisibleItem() 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); + + // Api not intended for QML use + // can't do overloads in QML + QQuickItem *removeItem(QQuickItem *item); + QQuickItem *removeItem(int item); + + // 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); + + /** + * 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 an 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 it can either be a pointer of an item or an integer specifying the position to remove + * @returns the item that has just been removed + */ + QQuickItem *removeItem(const QVariant &item); + + /** + * Removes all the items after item. Starting from the last column, every column will be removed until item is found, which will be left in place. + * 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 + * @param item the item which will be the new last one of the row. + * @returns the last item that has been removed + */ + QQuickItem *pop(QQuickItem *item); + + /** + * 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; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; +#else + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; +#endif + 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 firstVisibleItemChanged(); + void lastVisibleItemChanged(); + void topPaddingChanged(); + void bottomPaddingChanged(); + +private: + static void contentChildren_append(QQmlListProperty *prop, QQuickItem *object); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + static int contentChildren_count(QQmlListProperty *prop); + static QQuickItem *contentChildren_at(QQmlListProperty *prop, int index); +#else + static qsizetype contentChildren_count(QQmlListProperty *prop); + static QQuickItem *contentChildren_at(QQmlListProperty *prop, qsizetype index); +#endif + static void contentChildren_clear(QQmlListProperty *prop); + + static void contentData_append(QQmlListProperty *prop, QObject *object); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + static int contentData_count(QQmlListProperty *prop); + static QObject *contentData_at(QQmlListProperty *prop, int index); +#else + static qsizetype contentData_count(QQmlListProperty *prop); + static QObject *contentData_at(QQmlListProperty *prop, qsizetype index); +#endif + 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/columnview_p.h b/src/columnview_p.h new file mode 100644 index 0000000..48c9961 --- /dev/null +++ b/src/columnview_p.h @@ -0,0 +1,93 @@ +/* + * 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 { +class Units; +} + +class QmlComponentsPool : public QObject +{ + Q_OBJECT + +public: + QmlComponentsPool(QQmlEngine *engine); + ~QmlComponentsPool() override; + + QQmlComponent *m_separatorComponent = nullptr; + QQmlComponent *m_rightSeparatorComponent = nullptr; + Kirigami::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 *ensureSeparator(QQuickItem *item); + QQuickItem *ensureRightSeparator(QQuickItem *item); + + void setBoundedX(qreal x); + void animateX(qreal x); + void snapToItem(); + + inline qreal viewportLeft() const; + inline qreal viewportRight() const; + +protected: + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) override; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; +#else + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; +#endif + +private Q_SLOTS: + void syncItemsOrder(); + void updateRepeaterModel(); + +private: + ColumnView *m_view; + QPropertyAnimation *m_slideAnim; + QList m_items; + QList m_visibleItems; + QPointer m_viewAnchorItem; + QHash m_separators; + QHash m_rightSeparators; + 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; + friend class ColumnView; +}; diff --git a/src/controls/AboutItem.qml b/src/controls/AboutItem.qml new file mode 100644 index 0000000..5dcc48d --- /dev/null +++ b/src/controls/AboutItem.qml @@ -0,0 +1,373 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Controls 2.4 as QQC2 +import QtQuick.Window 2.15 +import QtQuick.Layouts 1.3 +import org.kde.kirigami 2.18 + +/** + * @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 stard with "org.kde.", otherwise is empty.` + */ + property url getInvolvedUrl: aboutData.desktopFileName.startsWith("org.kde.") ? "https://community.kde.org/Get_Involved" : "" + + /** @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 + + implicitHeight: form.implicitHeight + implicitWidth: form.implicitWidth + + Component { + id: personDelegate + + RowLayout { + Layout.fillWidth: true + property bool hasRemoteAvatar: (typeof(modelData.ocsUsername) !== "undefined" && modelData.ocsUsername.length > 0) + + spacing: Units.smallSpacing * 2 + + Icon { + id: avatarIcon + + implicitWidth: Units.iconSizes.medium + implicitHeight: implicitWidth + + fallback: "user" + source: hasRemoteAvatar && remoteAvatars.checked ? "https://store.kde.org/avatar/%1?s=%2".arg(modelData.ocsUsername).arg(width) : "user" + visible: status !== Icon.Loading + } + + // So it's clear that something is happening while avatar images are loaded + QQC2.BusyIndicator { + implicitWidth: Units.iconSizes.medium + implicitHeight: implicitWidth + + visible: avatarIcon.status === 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.ocsUsername) !== "undefined" && modelData.ocsUsername.length > 0 + icon.name: "get-hot-new-stuff" + QQC2.ToolTip.delay: Units.toolTipDelay + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.text: qsTr("Visit %1's KDE Store page").arg(modelData.name) + onClicked: Qt.openUrlExternally("https://store.kde.org/u/%1".arg(modelData.ocsUsername)) + } + + QQC2.ToolButton { + visible: typeof(modelData.emailAddress) !== "undefined" && modelData.emailAddress.length > 0 + icon.name: "mail-sent" + QQC2.ToolTip.delay: 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: Units.toolTipDelay + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.text: (typeof(modelData.webAddress) === "undefined" && modelData.webAddress.length > 0) ? "" : modelData.webAddress + onClicked: Qt.openUrlExternally(modelData.webAddress) + } + } + } + + FormLayout { + id: form + + anchors.fill: parent + + GridLayout { + columns: 2 + Layout.fillWidth: true + + Icon { + Layout.rowSpan: 3 + Layout.preferredHeight: Units.iconSizes.huge + Layout.preferredWidth: height + Layout.maximumWidth: aboutItem.width / 3; + Layout.rightMargin: Units.largeSpacing + source: Settings.applicationWindowIcon || aboutItem.aboutData.programLogo || aboutItem.aboutData.programIconName || aboutItem.aboutData.componentName + } + + Heading { + Layout.fillWidth: true + text: aboutItem.aboutData.displayName + " " + aboutItem.aboutData.version + wrapMode: Text.WordWrap + } + + Heading { + Layout.fillWidth: true + level: 2 + wrapMode: Text.WordWrap + text: aboutItem.aboutData.shortDescription + } + + UrlButton { + text: qsTr("Get Involved") + url: aboutItem.getInvolvedUrl + visible: url !== "" + } + } + + Separator { + Layout.fillWidth: true + } + + Heading { + FormData.isSection: true + text: qsTr("Copyright") + } + + QQC2.Label { + Layout.leftMargin: Units.gridUnit + text: aboutData.otherText + visible: text.length > 0 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + QQC2.Label { + Layout.leftMargin: Units.gridUnit + text: aboutData.copyrightStatement + visible: text.length > 0 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + UrlButton { + Layout.leftMargin: Units.gridUnit + url: aboutData.homepage + visible: url.length > 0 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + OverlaySheet { + id: licenseSheet + property alias text: bodyLabel.text + property alias name: heading.text + + header: Heading { + id: heading + } + + contentItem: QQC2.Label { + id: bodyLabel + text: licenseSheet.text + wrapMode: Text.Wrap + } + } + + Component { + id: licenseLinkButton + + RowLayout { + Layout.leftMargin: Units.smallSpacing + + QQC2.Label { text: qsTr("License:") } + + LinkButton { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: modelData.name + onClicked: { + licenseSheet.text = modelData.text + licenseSheet.name = modelData.name + licenseSheet.open() + } + } + } + } + + Component { + id: licenseTextItem + + QQC2.Label { + Layout.leftMargin: Units.smallSpacing + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: qsTr("License: %1").arg(modelData.name) + } + } + + Repeater { + model: aboutData.licenses + delegate: _usePageStack ? licenseLinkButton : licenseTextItem + } + + Heading { + FormData.isSection: visible + text: qsTr("Libraries in use") + Layout.fillWidth: true + wrapMode: Text.WordWrap + visible: Settings.information + } + + Repeater { + model: Settings.information + delegate: QQC2.Label { + Layout.leftMargin: Units.gridUnit + Layout.fillWidth: true + wrapMode: Text.WordWrap + id: libraries + text: modelData + } + } + + Repeater { + model: aboutData.components + delegate: QQC2.Label { + Layout.fillWidth: true + wrapMode: Text.WordWrap + Layout.leftMargin: Units.gridUnit + text: modelData.name + (modelData.version === "" ? "" : " %1".arg(modelData.version)) + } + } + + Heading { + Layout.fillWidth: true + FormData.isSection: visible + text: qsTr("Authors") + wrapMode: Text.WordWrap + visible: aboutData.authors.length > 0 + } + + QQC2.CheckBox { + id: remoteAvatars + visible: authorsRepeater.hasAnyRemoteAvatars + checked: false + text: qsTr("Show author photos") + + Timer { + id: remotesThrottle + repeat: false + interval: 1 + onTriggered: { + var hasAnyRemotes = false; + for (var i = 0; i < authorsRepeater.count; ++i) { + var itm = authorsRepeater.itemAt(i); + if (itm.hasRemoteAvatar) { + hasAnyRemotes = true; + break; + } + } + authorsRepeater.hasAnyRemoteAvatars = hasAnyRemotes; + } + } + } + + Repeater { + id: authorsRepeater + model: aboutData.authors + property bool hasAnyRemoteAvatars + delegate: personDelegate + onCountChanged: remotesThrottle.start() + } + + Heading { + height: visible ? implicitHeight : 0 + FormData.isSection: visible + text: qsTr("Credits") + visible: repCredits.count > 0 + } + + Repeater { + id: repCredits + model: aboutData.credits + delegate: personDelegate + } + + Heading { + height: visible ? implicitHeight : 0 + FormData.isSection: visible + text: qsTr("Translators") + visible: repTranslators.count > 0 + } + + Repeater { + id: repTranslators + model: aboutData.translators + delegate: personDelegate + } + } +} diff --git a/src/controls/AboutPage.qml b/src/controls/AboutPage.qml new file mode 100644 index 0000000..a93807c --- /dev/null +++ b/src/controls/AboutPage.qml @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Controls 2.4 as QQC2 +import QtQuick.Window 2.15 +import QtQuick.Layouts 1.3 +import org.kde.kirigami 2.19 + +/** + * @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 + */ +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 + + /** @internal */ + default property alias _content: aboutItem._content +//END properties + + title: qsTr("About %1").arg(page.aboutData.displayName) + + actions.main: Action { + text: qsTr("Report Bug…") + icon.name: "tools-report-bug" + onTriggered: { + if (page.aboutData.bugAddress === "submit@bugs.kde.org") { + const elements = page.aboutData.productName.split('/'); + let url = `https://bugs.kde.org/enter_bug.cgi?format=guided&product=${elements[0]}&version=${page.aboutData.version}`; + if (elements.length === 2) { + url += "&component=" + elements[1] + } + Qt.openUrlExternally(url) + } else { + Qt.openUrlExternally(page.aboutData.bugAddress) + } + } + } + + 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..2d4afe6 --- /dev/null +++ b/src/controls/AbstractApplicationHeader.qml @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import org.kde.kirigami 2.5 + +import "private" +import "templates" as T + + +/** + * @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 org::kde::kirigami::templates::AbstractApplicationHeader + */ +T.AbstractApplicationHeader { + id: root + + Theme.inherit: false + Theme.colorSet: Theme.Header + + background: Rectangle { + color: Theme.backgroundColor + EdgeShadow { + id: shadow + visible: root.separatorVisible + anchors { + right: parent.right + left: parent.left + top: parent.bottom + } + edge: Qt.TopEdge + opacity: (!root.page.header || root.page.header.toString().indexOf("ToolBar") === -1) + Behavior on opacity { + OpacityAnimator { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + Behavior on opacity { + OpacityAnimator { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + } +} + diff --git a/src/controls/AbstractApplicationItem.qml b/src/controls/AbstractApplicationItem.qml new file mode 100644 index 0000000..166c8f8 --- /dev/null +++ b/src/controls/AbstractApplicationItem.qml @@ -0,0 +1,469 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQml 2.14 +import QtQuick.Templates 2.12 as T +import QtQuick.Window 2.12 +import "templates/private" +import org.kde.kirigami 2.14 + +/** + * @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 2.4 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 exists for compatibility with Applicationwindow. + * + * default: ``Window.activeFocusItem`` + */ + readonly property Item activeFocusItem: Window.activeFocusItem + + /** + * @brief This property holds the font for this item. + * + * default: ``Theme.defaultFont`` + */ + property font font: Theme.defaultFont + + /** + * @brief This property holds the palette for this item. + * + * default: ``Theme.palette`` + */ + property var palette: Theme.palette + + /** + * @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. + * @warning This will be restricted to QQC2.MenuBar in KF6. + */ + property Item menuBar // TODO KF6 restrict type to QQC2.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 Units.gridUnit * 1.6 + * * ``Layout.maximumHeight``: default is Units.gridUnit * 3 + * + * To achieve a titlebar that stays completely fixed, just set the 3 sizes as the same. + * + * @warning This will be restricted to Kirigami.ApplicationHeader in KF6. + * @property org::kde:kirigami::ApplicationHeader header + */ + property Item header // TODO KF6 restrict the type to Kirigami.ApplicationHeader + + /** + * @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 >= 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 class 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 2.4 as Kirigami + * + * Kirigami.ApplicationItem { + * [...] + * contextDrawer: Kirigami.ContextDrawer { + * id: contextDrawer + * } + * [...] + * } + * @endcode + * + * @code + * import org.kde.kirigami 2.4 as Kirigami + * + * Kirigami.Page { + * [...] + * contextualActions: [ + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * }, + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * } + * ] + * [...] + * } + * @endcode + * + * When this page will be the current one, the context drawer will visualize + * contextualActions defined as property in that page. + * + * @property org::kde::kirigami::ContextDrawer contextDrawer + */ + property OverlayDrawer contextDrawer + + /** + * @brief This tells us whether the application is in reachable mode for single hand use. + * + * The whole content of the application is moved down the screen to be + * reachable with the thumb. If wideScreen is true, or reachableModeEnabled is false, + * this property has no effect. + * + * default: ``false`` + */ + property bool reachableMode: false + + /** + * @brief This property sets whether the application will go into reachable mode on pull down. + * + * default: ``true`` + */ + property bool reachableModeEnabled: true + + /** + * @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 + } + + transform: Translate { + Behavior on y { + NumberAnimation { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + y: root.reachableMode && root.reachableModeEnabled && !root.wideScreen ? root.height/2 : 0 + x: root.globalDrawer && root.globalDrawer.modal === true && root.globalDrawer.toString().indexOf("SplitDrawer") === 0 ? root.globalDrawer.contentItem.width * root.globalDrawer.position : 0 + } + } + + /** + * @brief This property holds the color for the background. + * + * default: ``Theme.backgroundColor`` + */ + property color color: 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) { + if (!internal.__passiveNotification) { + const component = Qt.createComponent("templates/private/PassiveNotification.qml"); + internal.__passiveNotification = component.createObject(root); + } + + internal.__passiveNotification.showNotification(message, timeout, actionText, callBack); + } + + /** + * @brief This function hides the passive notification, if any is shown. + */ + function hidePassiveNotification() { + if(internal.__passiveNotification) { + internal.__passiveNotification.hideNotification(); + } + } + + /** + * @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: { + menuBar.parent = root.contentItem + if (menuBar.z === undefined) { + menuBar.z = 1; + } + if (menuBar instanceof T.ToolBar) { + menuBar.position = T.ToolBar.Footer + } else if (menuBar instanceof T.TabBar) { + menuBar.position = T.TabBar.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: { + header.parent = root.contentItem + if (header.z === undefined) { + header.z = 1; + } + if (header instanceof T.ToolBar) { + header.position = T.ToolBar.Header + } else if (header instanceof T.TabBar) { + header.position = T.TabBar.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: { + footer.parent = root.contentItem + if (footer.z === undefined) { + footer.z = 1; + } + if (footer instanceof T.ToolBar) { + footer.position = T.ToolBar.Footer + } else if (footer instanceof T.TabBar) { + footer.position = T.TabBar.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: { + background.parent = root.contentItem + if (background.z === undefined) { + background.z = -1; + } + background.anchors.fill = background.parent + } + + // NOTE: Don't want overscroll in landscape mode + onWidthChanged: { + if (width > height) { + root.reachableMode = false; + } + } + + onPageStackChanged: pageStack.parent = root.contentItem; +//END signals handlers + + LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft + LayoutMirroring.childrenInherit: true + + Item { + anchors.fill: parent + parent: root.parent || root + z: 999999 + Rectangle { + z: -1 + anchors.fill: parent + color: "black" + visible: contextDrawer && contextDrawer.modal + parent: contextDrawer ? contextDrawer.background.parent.parent : overlayRoot + opacity: contextDrawer ? contextDrawer.position * 0.6 : 0 + } + Rectangle { + z: -1 + anchors.fill: parent + color: "black" + visible: globalDrawer && globalDrawer.modal + parent: globalDrawer ? globalDrawer.background.parent.parent : overlayRoot + opacity: globalDrawer ? globalDrawer.position * 0.6 : 0 + } + Item { + id: overlayRoot + z: -1 + anchors.fill: parent + } + Window.onWindowChanged: { + if (globalDrawer) { + globalDrawer.visible = globalDrawer.drawerOpen; + } + if (contextDrawer) { + contextDrawer.visible = contextDrawer.drawerOpen; + } + } + } + + MouseArea { + parent: root + z: -1 + anchors.fill: parent + onClicked: root.reachableMode = false; + visible: root.reachableMode && root.reachableModeEnabled + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.3) + opacity: 0.15 + Icon { + anchors.horizontalCenter: parent.horizontalCenter + y: x + width: Units.iconSizes.large + height: width + source: "go-up" + } + } + } + + Binding { + when: globalDrawer !== undefined && root.visible + target: globalDrawer + property: "parent" + value: overlay + restoreMode: Binding.RestoreBinding + } + Binding { + when: contextDrawer !== undefined && root.visible + target: contextDrawer + property: "parent" + value: overlay + restoreMode: Binding.RestoreBinding + } + + implicitWidth: Units.gridUnit * 30 + implicitHeight: Units.gridUnit * 45 + visible: true + + QtObject { + id: internal + property QtObject __passiveNotification + } +} diff --git a/src/controls/AbstractApplicationWindow.qml b/src/controls/AbstractApplicationWindow.qml new file mode 100644 index 0000000..092ebf0 --- /dev/null +++ b/src/controls/AbstractApplicationWindow.qml @@ -0,0 +1,349 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Controls 2.0 as QQC2 +import QtQuick.Window 2.5 +import "templates/private" +import org.kde.kirigami 2.4 + +/** + * 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 + * a top ApplicationHeader, as well as 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 2.4 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 >= 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 class 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 2.4 as Kirigami + * + * Kirigami.ApplicationWindow { + * [...] + * contextDrawer: Kirigami.ContextDrawer { + * id: contextDrawer + * } + * [...] + * } + * @endcode + * + * @code + * import org.kde.kirigami 2.4 as Kirigami + * + * Kirigami.Page { + * [...] + * contextualActions: [ + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * }, + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * } + * ] + * [...] + * } + * @endcode + * + * When this page will be the current one, the context drawer will visualize + * contextualActions defined as property in that page. + * @property org::kde::kirigami::ContextDrawer contextDrawer + */ + property OverlayDrawer contextDrawer + + /** + * @brief This property tells whether the application is in reachable mode for single hand use. + * + * The whole content of the application is moved down the screen to be + * reachable with the thumb. If wideScreen is true, or reachableModeEnabled is false, + * this property has no effect. + * + * default: ``false`` + */ + property bool reachableMode: false + + /** + * @brief This property sets whether the application will go into reachable mode on pull down. + */ + property bool reachableModeEnabled: true + + /** + * 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 Action quitAction: _quitAction +//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) { + if (!internal.__passiveNotification) { + var component = Qt.createComponent("templates/private/PassiveNotification.qml"); + internal.__passiveNotification = component.createObject(overlay.parent); + } + + internal.__passiveNotification.showNotification(message, timeout, actionText, callBack); + } + + /** + * @brief This function hides the passive notification, if any is shown. + */ + function hidePassiveNotification() { + if(internal.__passiveNotification) { + internal.__passiveNotification.hideNotification(); + } + } + + + /** + * @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: Theme.backgroundColor + + MouseArea { + parent: contentItem.parent + z: 0 + anchors.fill: parent + onClicked: root.reachableMode = false; + visible: root.reachableMode && root.reachableModeEnabled + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.3) + opacity: 0.15 + Icon { + anchors.horizontalCenter: parent.horizontalCenter + y: x + width: Units.iconSizes.large + height: width + source: "go-up" + } + } + } + + 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 { + when: menuBar !== undefined + target: menuBar + property: "x" + value: -contentItem.x + } + Binding { + when: header !== undefined + target: header + property: "x" + value: -contentItem.x + } + Binding { + when: footer !== undefined + target: footer + property: "x" + value: -contentItem.x + } + + contentItem.transform: Translate { + Behavior on y { + NumberAnimation { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + y: root.reachableMode && root.reachableModeEnabled && !root.wideScreen ? root.height/2 : 0 + x: root.globalDrawer && root.globalDrawer.modal === true && root.globalDrawer.toString().indexOf("SplitDrawer") === 0 ? root.globalDrawer.contentItem.width * root.globalDrawer.position : 0 + } + //Don't want overscroll in landscape mode + onWidthChanged: { + if (width > height) { + root.reachableMode = false; + } + } + Binding { + when: globalDrawer !== undefined && root.visible + target: globalDrawer + property: "parent" + value: overlay + } + Binding { + when: contextDrawer !== undefined && root.visible + target: contextDrawer + property: "parent" + value: overlay + } + onPageStackChanged: pageStack.parent = contentItem; + + width: Settings.isMobile ? Units.gridUnit * 30 : Units.gridUnit * 55 + height: Settings.isMobile ? Units.gridUnit * 45 : 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; + } + + QtObject { + id: internal + property QtObject __passiveNotification + } + + Action { + id: _quitAction + text: qsTr("Quit") + icon.name: "application-exit"; + shortcut: StandardKey.Quit + onTriggered: root.close() + } +} diff --git a/src/controls/AbstractCard.qml b/src/controls/AbstractCard.qml new file mode 100644 index 0000000..50c9f1c --- /dev/null +++ b/src/controls/AbstractCard.qml @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.6 +import org.kde.kirigami 2.12 +import "templates" as T +import "private" + +/** + * @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: DefaultCardBackground { + id: bg + + clickFeedback: root.showClickFeedback + hoverFeedback: root.hoverEnabled + } +} diff --git a/src/controls/AbstractChip.qml b/src/controls/AbstractChip.qml new file mode 100644 index 0000000..344e4aa --- /dev/null +++ b/src/controls/AbstractChip.qml @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2022 Felipe Kinoshita +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.19 as Kirigami +import "templates" as T +import "private" + +/** + * AbstractChip is a visual object based on AbstractButton + * that provides a way to display predetermined elements + * with the visual styling of "tags" or "tokens". It provides + * the look, the base properties, and signals for an AbstractButton. + * + * @inherit org::kde::kirigami::templates::AbstractChip + * @since 2.19 + */ +T.AbstractChip { + id: root + + background: DefaultChipBackground {} +} diff --git a/src/controls/AbstractItemViewHeader.qml b/src/controls/AbstractItemViewHeader.qml new file mode 100644 index 0000000..a2c28df --- /dev/null +++ b/src/controls/AbstractItemViewHeader.qml @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import QtQuick.Templates 2.0 as T2 +import org.kde.kirigami 2.4 as Kirigami + +/** + * @brief An item that can be used as an header for a ListView. + * + * It will play nice with the margin policies of ScrollablePage and can + * automatically shrink when the list is scrolled, like the behavior + * of list headers in many mobile applications. + * @since 2.1 + * @inherit QtQuick.Controls.Control + * @deprecated since 5.97; Don't use AbstractItemViewHeader in your views anymore. + * + * TODO KF6: remove + */ +T2.Control { + /** + * @brief This property holds the minimum height of the AbstractItemViewHeader. + */ + property int minimumHeight: Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing * 2 + + /** + * @brief This property holds the maximum height of the AbstractItemViewHeader. + */ + property int maximumHeight: Kirigami.Units.gridUnit * 6 + + /** + * @brief This property holds the ListView for which this item is the header. + * + * By default automatically set to the attached property: `ListView.view`. + */ + property ListView view: ListView.view + + width: view.width + + implicitHeight: topPadding + bottomPadding + (view.headerPositioning === ListView.InlineHeader + ? maximumHeight + : Math.min(maximumHeight, Math.max(minimumHeight, maximumHeight - Math.max(0, view.contentY)))) + + + z: 9 + topPadding: applicationWindow() && !applicationWindow().wideScreen && applicationWindow().header ? applicationWindow().header.paintedHeight : 0 + rightPadding: Kirigami.Units.gridUnit + +} diff --git a/src/controls/AbstractListItem.qml b/src/controls/AbstractListItem.qml new file mode 100644 index 0000000..2f8a57f --- /dev/null +++ b/src/controls/AbstractListItem.qml @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import "private" +import "templates" as T + +/** + * @brief An item delegate for the primitive ListView component. + * + * It's intended to make all listviews look coherent. + * + * @inherit org::kde::kirigami::templates::AbstractListItem + */ +T.AbstractListItem { + id: listItem + + background: DefaultListItemBackground {} +} diff --git a/src/controls/Action.qml b/src/controls/Action.qml new file mode 100644 index 0000000..d8a421d --- /dev/null +++ b/src/controls/Action.qml @@ -0,0 +1,191 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.7 +import QtQuick.Controls 2.4 as Controls +import "private" +import org.kde.kirigami 2.14 as Kirigami + +/** + * @brief An item that represents an abstract Action + * @inherit QtQuick.Controls.Action + */ +Controls.Action { + id: root + +//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: true + + /** + * @brief This property holds the icon name for the action. This will pick the icon with the given name from the current theme. + * @deprecated Use icon.name instead. + * @property string iconName + */ + property alias iconName: root.icon.name + + /** + * @brief This property holds an url to an icon file or resource url for the action. + * @note Use this if you want a specific file rather than an icon from the theme. + * @deprecated Use icon.name instead. + * @property url iconSource + */ + property alias iconSource: root.icon.source + + /** + * @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 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 Controls.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: ``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 is a helper function to check if a certain display hint has been set. + * + * This function is mostly convenience to enforce the mutual exclusivity of KeepVisible and AlwaysHide. + * + * @param hint The display hint to check if it is set. + * @see org::kde::kirigami::DisplayHint + * @deprecated since 2.14, Use DisplayHint.displayHintSet(action, hint) instead. + * @return true if the hint was set for this action, false if not. + * @since 2.12 + */ + function displayHintSet(hint) { + print("Action::displayHintSet is deprecated, use DisplayHint.displayHintSet(action, hint)") + return Kirigami.DisplayHint.displayHintSet(root, hint); + } + + /** + * @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: null + + /** + * @brief This property holds a list of child actions. + * + * This is useful for tree-like menus, such as the GlobalDrawer. + * + * Example usage: + * @code + * Action { + * text: "Tools" + * Action { + * text: "Action1" + * } + * Action { + * text: "Action2" + * } + * } + * @endcode + * @property list children + */ + default property list children +//END properties + + onChildrenChanged: { + var child; + for (var i in children) { + child = children[i]; + if (child.hasOwnProperty("parent")) { + child.parent = root + } + } + } + + /** + * @brief This property holds the action's visible child actions. + * @property list visibleChildren + */ + readonly property var visibleChildren: { + let visible = []; + for (let i in children) { + const child = children[i]; + if (!child.hasOwnProperty("visible") || child.visible) { + visible.push(child); + } + } + return visible; + } + + /** + * @brief Hints for implementations using Actions indicating preferences about how to display the action. + * @see org::kde::kirigami::DisplayHint + * @deprecated since 2.14, use Kirigami.DisplayHint instead. + */ + enum DisplayHint { + /** + * 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 + } +} diff --git a/src/controls/ActionTextField.qml b/src/controls/ActionTextField.qml new file mode 100644 index 0000000..4807ffb --- /dev/null +++ b/src/controls/ActionTextField.qml @@ -0,0 +1,178 @@ +/* + * SPDX-FileCopyrightText: 2019 Carl-Lucien Schwan + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as Controls + +import org.kde.kirigami 2.20 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 2.20 as Kirigami + * + * Kirigami.ActionTextField { + * id: searchField + * + * placeholderText: i18n("Search...") + * + * focusSequence: "Ctrl+F" + * + * rightActions: Kirigami.Action { + * icon.name: "edit-clear" + * visible: searchField.text !== "" + * onTriggered: { + * searchField.text = "" + * searchField.accepted() + * } + * } + * + * onAccepted: console.log("Search text is " + searchField.text) + * } + * @endcode + * + * @since 5.56 + * @inherit QtQuick.Controls.TextField + */ +Controls.TextField +{ + id: root + + /** + * @brief This property holds a shortcut sequence that will focus the text field. + * @since 5.56 + */ + property string focusSequence + + /** + * @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 + + horizontalAlignment: Qt.AlignLeft + LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft + LayoutMirroring.childrenInherit: true + + leftPadding: Kirigami.Units.smallSpacing + (LayoutMirroring.enabled ? rightActionsRow : leftActionsRow).width + rightPadding: Kirigami.Units.smallSpacing + (LayoutMirroring.enabled ? 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.focusSequence + sequence: root.focusSequence + onActivated: { + root.forceActiveFocus() + root.selectAll() + } + } + + Controls.ToolTip { + visible: root.focusSequence && root.text.length === 0 && !rightActionsRow.hovered && !leftActionsRow.hovered && hovered + text: root.focusSequence ? root.focusSequence : "" + } + + Row { + id: leftActionsRow + padding: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing + layoutDirection: Qt.LeftToRight + 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 + Kirigami.Icon { + 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 + active: leftActionArea.containsPress + visible: modelData.visible + enabled: modelData.enabled + MouseArea { + id: leftActionArea + anchors.fill: parent + onClicked: modelData.trigger() + cursorShape: Qt.PointingHandCursor + } + } + } + } + + Row { + id: rightActionsRow + padding: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing + layoutDirection: Qt.RightToLeft + 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 + Kirigami.Icon { + 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 + active: rightActionArea.containsPress + visible: modelData.visible + enabled: modelData.enabled + MouseArea { + id: rightActionArea + anchors.fill: parent + onClicked: modelData.trigger() + cursorShape: Qt.PointingHandCursor + } + } + } + } +} diff --git a/src/controls/ActionToolBar.qml b/src/controls/ActionToolBar.qml new file mode 100644 index 0000000..ed7f1cf --- /dev/null +++ b/src/controls/ActionToolBar.qml @@ -0,0 +1,250 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.7 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.4 as QQC2 +import org.kde.kirigami 2.14 as Kirigami +import "private" + +/** + * @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 ActionToolBar component will try to display has many actions as possible but + * 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 + */ + property alias actions: layout.actions + + /** + * @brief This property holds a list of hidden actions. + * + * These actions will always be displayed in the overflow menu, even if there is enough space. + * + * @deprecated since 2.14, use the AlwaysHide hint on actions instead. + * @property list hiddenActions + * @since 2.6 + */ + property list hiddenActions + onHiddenActionsChanged: print("ActionToolBar::hiddenActions is deprecated, use the AlwaysHide hint on your actions instead") + + /** + * @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.AlignRight`` + * + * @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 && parent.hasOwnProperty("position") + ? 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 + */ + 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 + + contentItem: Kirigami.ToolBarLayout { + id: layout + spacing: Kirigami.Units.smallSpacing + layoutDirection: root.LayoutMirroring.enabled ? Qt.RightToLeft : Qt.LeftToRight + + fullDelegate: PrivateActionToolButton { + flat: root.flat + display: root.display + action: Kirigami.ToolBarLayout.action + } + + iconDelegate: PrivateActionToolButton { + flat: root.flat + display: QQC2.Button.IconOnly + action: Kirigami.ToolBarLayout.action + + showMenuArrow: false + + menuActions: { + if (action.displayComponent) { + return [action] + } + + if (action.hasOwnProperty("children") && action.children.length > 0) { + return Array.prototype.map.call(action.children, i => i) + } + + return [] + } + } + + moreButton: PrivateActionToolButton { + flat: root.flat + + action: Kirigami.Action { + tooltip: qsTr("More Actions") + icon.name: root.overflowIconName + displayHint: Kirigami.DisplayHint.IconOnly | Kirigami.DisplayHint.HideChildIndicator + } + + menuActions: { + if (root.hiddenActions.length === 0) { + return root.actions + } else { + result = [] + result.concat(Array.prototype.map.call(root.actions, (i) => i)) + result.concat(Array.prototype.map.call(hiddenActions, (i) => i)) + return result + } + } + + menuComponent: ActionsMenu { + submenuComponent: ActionsMenu { + Binding { + target: parentItem + property: "visible" + value: layout.hiddenActions.includes(parentAction) + && (parentAction.visible === undefined || parentAction.visible) + } + } + + itemDelegate: ActionMenuItem { + visible: layout.hiddenActions.includes(action) + && (action.visible === undefined || action.visible) + } + + loaderDelegate: Loader { + property var action + height: visible ? implicitHeight : 0 + visible: layout.hiddenActions.includes(action) + && (action.visible === undefined || action.visible) + } + + separatorDelegate: QQC2.MenuSeparator { + property var action + visible: layout.hiddenActions.includes(action) + && (action.visible === undefined || action.visible) + } + } + } + } +} diff --git a/src/controls/ApplicationHeader.qml b/src/controls/ApplicationHeader.qml new file mode 100644 index 0000000..b8658f2 --- /dev/null +++ b/src/controls/ApplicationHeader.qml @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import "templates" as T + + +/** + * @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 org::kde::kirigami::templates::ApplicationHeader + */ +T.ApplicationHeader { + id: header +} diff --git a/src/controls/ApplicationItem.qml b/src/controls/ApplicationItem.qml new file mode 100644 index 0000000..e2d277c --- /dev/null +++ b/src/controls/ApplicationItem.qml @@ -0,0 +1,154 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import "templates/private" +import org.kde.kirigami 2.4 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 2.4 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 +*/ +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. + * + * @warning This property is readonly. + * @property QtQuick.StackView ApplicationItem::pageStack + */ + property alias pageStack: __pageStack // TODO KF6 make readonly + + // Redefines here as here we can know a pointer to PageRow + wideScreen: width >= applicationWindow().pageStack.defaultColumnWidth * 2 + + Component.onCompleted: { + if (pageStack.currentItem) { + pageStack.currentItem.forceActiveFocus(); + } + } + + PageRow { + id: __pageStack + anchors { + fill: parent + // HACK: workaround a bug in android iOS keyboard management + bottomMargin: ((Qt.platform.os === "android" || Qt.platform.os === "ios") || !Qt.inputMethod.visible) ? 0 : Qt.inputMethod.keyboardRectangle.height + onBottomMarginChanged: { + if (bottomMargin > 0) { + root.reachableMode = false; + } + } + } + // FIXME + onCurrentIndexChanged: root.reachableMode = false; + + 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: { + 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..d85c48b --- /dev/null +++ b/src/controls/ApplicationWindow.qml @@ -0,0 +1,133 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import "templates/private" +import org.kde.kirigami 2.4 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 2.4 as Kirigami + * + * Kirigami.ApplicationWindow { + * [...] + * globalDrawer: Kirigami.GlobalDrawer { + * actions: [ + * Kirigami.Action { + * text: "View" + * iconName: "view-list-icons" + * Kirigami.Action { + * text: "action 1" + * } + * Kirigami.Action { + * text: "action 2" + * } + * Kirigami.Action { + * text: "action 3" + * } + * }, + * Kirigami.Action { + * text: "Sync" + * iconName: "folder-sync" + * } + * ] + * } + * + * contextDrawer: Kirigami.ContextDrawer { + * id: contextDrawer + * } + * + * pageStack.initialPage: Kirigami.Page { + * mainAction: Kirigami.Action { + * iconName: "edit" + * onTriggered: { + * // do stuff + * } + * } + * contextualActions: [ + * Kirigami.Action { + * iconName: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * }, + * Kirigami.Action { + * iconName: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * } + * ] + * [...] + * } + * [...] + * } + * @endcode + * +*/ +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. + * + * @warning This property is not currently readonly, but it should be treated like it is readonly. + * @property org::kde::kirigami::PageRow pageStack + */ + property alias pageStack: __pageStack // TODO KF6 make readonly + + // 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: { + if (pageStack.currentItem) { + pageStack.currentItem.forceActiveFocus() + } + } + + PageRow { + id: __pageStack + globalToolBar.style: Kirigami.ApplicationHeaderStyle.Auto + anchors { + fill: parent + // HACK: workaround a bug in android iOS keyboard management + bottomMargin: ((Qt.platform.os === "android" || Qt.platform.os === "ios") || !Qt.inputMethod.visible) ? 0 : Qt.inputMethod.keyboardRectangle.height + onBottomMarginChanged: { + if (__pageStack.anchors.bottomMargin > 0) { + root.reachableMode = false; + } + } + } + // FIXME + onCurrentIndexChanged: root.reachableMode = false; + + focus: true + } +} diff --git a/src/controls/Avatar.qml b/src/controls/Avatar.qml new file mode 100644 index 0000000..5f992e6 --- /dev/null +++ b/src/controls/Avatar.qml @@ -0,0 +1,335 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.13 +import org.kde.kirigami 2.14 as Kirigami +import QtQuick.Controls 2.13 as QQC2 +import QtGraphicalEffects 1.0 +import org.kde.kirigami.private 2.14 +import "templates/private" as P + +/** + * @biref An element that represents a user, either with initials, an icon, or a profile image. + * @inherit QtQuick.Controls.Control + */ +QQC2.Control { + id: avatarRoot + + enum ImageMode { + AlwaysShowImage, + AdaptiveImageOrInitals, + AlwaysShowInitials + } + enum InitialsMode { + UseInitials, + UseIcon + } + +//BEGIN properties + /** + * @brief This property holds the given name of a user. + * + * The user's name will be used for generating initials and to provide the + * accessible name for assistive technology. + */ + property string name + + /** + * @brief This property holds the source of the user's profile picture; an image. + * @see QtQuick.Image::source + * @property url source + */ + property alias source: avatarImage.source + + /** + * @brief This property holds avatar's icon source. + * + * This icon is displayed when using an icon with ``Avatar.InitialsMode.UseIcon`` and + * ``Avatar.ImageNode.AlwaysShowInitials`` enabled. + * + * @see org::kde::kirigami::Icon::source + * @property var iconSource + */ + property alias iconSource: avatarIcon.source + + /** + * @brief This property holds how the button should represent the user when no user-set image is available. + * + * Possible values are: + * * ``Avatar.InitialsMode.UseInitials``: Show the user's initials. + * * ``Avatar.InitialsMode.UseIcon``: Show a generic icon. + * + * @see org::kde::kirigami::Avatar::InitialsMode + */ + property int initialsMode: Kirigami.Avatar.InitialsMode.UseInitials + + /** + * @brief This property holds how the avatar should be shown. + * + * This property holds whether the button should always show the image; show the image if one is + * available and show initials when it is not; or always show initials. + * + * Possible values are: + * * ``Avatar.ImageMode.AlwaysShowImage``: Always try to show the image; even if it hasn't loaded yet or is undefined. + * * ``Avatar.ImageMode.AdaptiveImageOrInitals``: Show the image if it is valid; or show initials if it is not + * * ``Avatar.ImageMode.AlwaysShowInitials``: Always show initials + * + * @see org::kde::kirigami::Avatar::ImageMode + */ + property int imageMode: Kirigami.Avatar.ImageMode.AdaptiveImageOrInitals + + /** + * @brief This property sets whether the provided image should be cached. + * @see QtQuick.Image::cache + * @property bool cache + */ + property alias cache: avatarImage.cache + + /** + * @brief This property holds the source size of the user's profile picture. + * @see QtQuick.Image::sourceSize + * @property int sourceSize + */ + property alias sourceSize: avatarImage.sourceSize + + /** + * @brief This property holds whether the provided image should be smoothed. + * @see QtQuick.Image::smooth + * @property bool smooth + */ + property alias smooth: avatarImage.smooth + + /** + * @brief This property holds the color to use for this avatar. + * + * If not explicitly set, this defaults to generating a color from the name. + * + * @property color color + */ + property var color: Kirigami.NameUtils.colorsFromString(name) + // We use a var instead of a color here to allow setting the colour + // as undefined, which will result in a generated colour being used. + + /** + * @brief This property holds the main and secondary actions associated with this avatar. + * @code + * Kirigami.Avatar { + * actions.main: Kirigami.Action {} + * actions.secondary: Kirigami.Action {} + * } + * @endcode + * + * Actions associated with this avatar. + * + * @note The secondary action should only be used for shortcuts of actions + * elsewhere in your application's UI, and cannot be accessed on mobile platforms. + */ + property AvatarGroup actions: AvatarGroup {} + + /** + * @brief This property holds the border properties group. + * @code + * Kirigami.Avatar { + * border.width: 10 + * border.color: 'red' + * } + * @endcode + */ + property P.BorderPropertiesGroup border: P.BorderPropertiesGroup { + width: 0 + color: Qt.rgba(0,0,0,0.2) + } +//END properties + + padding: 0 + horizontalPadding: padding + verticalPadding: padding + leftPadding: horizontalPadding + rightPadding: horizontalPadding + topPadding: verticalPadding + bottomPadding: verticalPadding + + implicitWidth: Kirigami.Units.iconSizes.large + implicitHeight: Kirigami.Units.iconSizes.large + + activeFocusOnTab: !!actions.main + + Accessible.role: !!actions.main ? Accessible.Button : Accessible.Graphic + Accessible.name: !!actions.main ? qsTr("%1 — %2").arg(name).arg(actions.main.text) : name + Accessible.focusable: !!actions.main + Accessible.onPressAction: { + avatarRoot.actions.main.trigger() + } + Keys.onEnterPressed: if (!!avatarRoot.actions.main.trigger()) avatarRoot.actions.main.trigger() + Keys.onSpacePressed: if (!!avatarRoot.actions.main.trigger()) avatarRoot.actions.main.trigger() + + background: Rectangle { + radius: parent.width / 2 + + color: __private.showImage ? Kirigami.Theme.backgroundColor : avatarRoot.color + + Rectangle { + anchors.fill: parent + anchors.margins: -border.width + + radius: width / 2 + + color: "transparent" + border.width: Kirigami.Units.smallSpacing + border.color: Kirigami.Theme.focusColor + visible: avatarRoot.focus + } + + MouseArea { + id: primaryMouse + + anchors.fill: parent + hoverEnabled: true + property bool mouseInCircle: { + let x = avatarRoot.width / 2, y = avatarRoot.height / 2 + let xPrime = mouseX, yPrime = mouseY + + let distance = (x - xPrime) ** 2 + (y - yPrime) ** 2 + let radiusSquared = (Math.min(avatarRoot.width, avatarRoot.height) / 2) ** 2 + + return distance < radiusSquared + } + + onClicked: { + if (mouseY > avatarRoot.height - secondaryRect.height && !!avatarRoot.actions.secondary) { + avatarRoot.actions.secondary.trigger() + return + } + if (!!avatarRoot.actions.main) { + avatarRoot.actions.main.trigger() + } + } + + enabled: !!avatarRoot.actions.main || !!avatarRoot.actions.secondary + cursorShape: containsMouse && mouseInCircle && enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + + states: [ + State { + name: "secondaryRevealed" + when: (!Kirigami.Settings.isMobile) && (!!avatarRoot.actions.secondary) && (primaryMouse.containsMouse && primaryMouse.mouseInCircle) + PropertyChanges { + target: secondaryRect + visible: true + } + } + ] + } + } + + QtObject { + id: __private + property color textColor: Kirigami.ColorUtils.brightnessForColor(avatarRoot.color) === Kirigami.ColorUtils.Light + ? "black" + : "white" + property bool showImage: { + return (avatarRoot.imageMode === Kirigami.Avatar.ImageMode.AlwaysShowImage) || + (avatarImage.status === Image.Ready && avatarRoot.imageMode === Kirigami.Avatar.ImageMode.AdaptiveImageOrInitals) + } + } + + contentItem: Item { + Text { + id: avatarText + fontSizeMode: Text.Fit + visible: avatarRoot.initialsMode === Kirigami.Avatar.InitialsMode.UseInitials && + !__private.showImage && + !Kirigami.NameUtils.isStringUnsuitableForInitials(avatarRoot.name) && + avatarRoot.width > Kirigami.Units.gridUnit + + text: Kirigami.NameUtils.initialsFromString(name) + color: __private.textColor + + anchors.fill: parent + font { + // this ensures we don't get a both point and pixel size are set warning + pointSize: -1 + pixelSize: (avatarRoot.height - Kirigami.Units.largeSpacing) / 2 + } + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + Kirigami.Icon { + id: avatarIcon + visible: (avatarRoot.initialsMode === Kirigami.Avatar.InitialsMode.UseIcon && !__private.showImage) || + (Kirigami.NameUtils.isStringUnsuitableForInitials(avatarRoot.name) && !__private.showImage) + + source: "user" + + anchors.fill: parent + anchors.margins: Kirigami.Units.largeSpacing + + color: __private.textColor + } + Image { + id: avatarImage + visible: __private.showImage + + mipmap: true + smooth: true + sourceSize { + width: avatarRoot.width + height: avatarRoot.height + } + + fillMode: Image.PreserveAspectCrop + anchors.fill: parent + } + + Rectangle { + color: "transparent" + + radius: width / 2 + anchors.fill: parent + + border { + width: avatarRoot.border.width + color: avatarRoot.border.color + } + } + + Rectangle { + id: secondaryRect + visible: false + + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + + height: Kirigami.Units.iconSizes.small + Kirigami.Units.smallSpacing*2 + + color: Qt.rgba(0, 0, 0, 0.6) + + Kirigami.Icon { + Kirigami.Theme.textColor: "white" + source: (avatarRoot.actions.secondary || {iconName: ""}).iconName + + width: Kirigami.Units.iconSizes.small + height: Kirigami.Units.iconSizes.small + + x: Math.round((parent.width/2)-(this.width/2)) + y: Math.round((parent.height/2)-(this.height/2)) + } + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + height: avatarRoot.height + width: avatarRoot.width + radius: height / 2 + color: "black" + visible: false + } + } + } +} diff --git a/src/controls/BasicListItem.qml b/src/controls/BasicListItem.qml new file mode 100644 index 0000000..dcb06b3 --- /dev/null +++ b/src/controls/BasicListItem.qml @@ -0,0 +1,345 @@ +/* + * SPDX-FileCopyrightText: 2010 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.8 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.0 as QQC2 +import org.kde.kirigami 2.12 + +/** + * @brief A BasicListItem provides a simple list item design that can handle the + * most common list item usecases. + * + * @image html BasicListItemTypes.svg "The styles of the BasicListItem. From left to right top to bottom: light icon + title + subtitle, dark icon + title + subtitle, light icon + label, dark icon + label, light label, dark label." width=50% + */ +AbstractListItem { + id: listItem + +//BEGIN properties + /** + * @brief This property holds the text of this list item's label. + * + * If a subtitle is provided, the label will behave as a title and will be styled + * accordingly. Every list item should have a label. + * + * @property string label + */ + property alias label: listItem.text + + /** + * @brief This property holds an optional subtitle that can appear under the label. + * @since 5.70 + * @since org.kde.kirigami 2.12 + */ + property alias subtitle: subtitleItem.text + + /** + * @brief This property holds an item that will be displayed before the title and subtitle. + * @note The leading item is allowed to expand infinitely horizontally, and should be bounded by the user. + * @since org.kde.kirigami 2.15 + */ + property Item leading + + /** + * @brief This property holds the padding after the leading item. + * @since org.kde.kirigami 2.15 + */ + property real leadingPadding: Units.largeSpacing + + // TODO KF6: remove this property and instead implement leading and trailing + // item positioning in such a way that they fill vertically, but a fixed + // height can be manually specified without needing to wrap it in an Item + /** + * @brief This property sets whether or not to stretch the leading item to fit all available vertical space. + * + * If false, you will be responsible for setting a height for the + * item or ensuring that its default height works. + * + * default: ``true`` + * + * @warning This property will likely be removed in KF6 + * @since 5.83 + * @since org.kde.kirigami 2.15 + */ + property bool leadingFillVertically: true + + /** + * @brief This property holds an item that will be displayed after the title and subtitle + * @note The trailing item is allowed to expand infinitely horizontally, and should be bounded by the user. + * @since org.kde.kirigami 2.15 + */ + property Item trailing + + /** + * @brief This property holds the padding before the trailing item. + * @since org.kde.kirigami 2.15 + */ + property real trailingPadding: Units.largeSpacing + + // TODO KF6: remove this property and instead implement leading and trailing + // item positioning in such a way that they fill vertically, but a fixed + // height can be manually specified without needing to wrap it in an Item + /** + * @brief This propery sets whether or not to stretch the trailing item to fit all available vertical space. + * + * If false, you will be responsible for setting a height for the + * item or ensuring that its default height works. + * + * default: ``true`` + * + * @warning This property will likely be removed in KF6 + * @since 5.83 + * @since org.kde.kirigami 2.15 + */ + property bool trailingFillVertically: true + + /** + * @brief This property sets whether list item's text should render in bold. + * + * default: ``false`` + * + * @since 5.71 + * @since org.kde.kirigami 2.13 + */ + property bool bold: false + + /** + * @code ts + * interface IconGroup { + * name: string, + * source: string, + * width: int, + * height: int, + * color: color, + * } + * + * type Icon = string | IconGroup | URL + * @endcode + * + * The icon that will render on this list item. + * + * This can either be an icon name, a URL, or an object with the following properties: + * + * If the type of the icon is a string containing an icon name, the icon will be looked up from the + * platform icon theme. + * + * Using an icon object allows you to specify more granular attributes of the icon, + * such as its color and dimensions. + * + * If the icon is a URL, the icon will be attempted to be loaded from the + * given URL. + */ + property var icon + + /** + * @brief This property sets the size at which the icon will render. + * + * This will not affect icon lookup, unlike the icon group's width and height properties, which will. + * + * @property int iconSize + * @since 2.5 + */ + property alias iconSize: iconItem.size + + /** + * @brief This property holds the color of the icon. + * + * If the icon's original colors should be left intact, set this to the default value, "transparent". + * Note that this colour will only be applied if the icon can be recoloured, (e.g. you can use Kirigami.Theme.foregroundColor to change the icon's colour.) + * + * @property color iconColor + * @since 2.7 + */ + property alias iconColor: iconItem.color + + /** + * @brief This property sets whether or not the icon has a "selected" appearance. + * + * Can be used to override the icon coloration if the list item's background and + * text are also being overridden, to ensure that the icon never becomes invisible. + * + * @since 5.91 + * @since org.kde.kirigami 2.19 + * @property bool iconSelected + */ + property alias iconSelected: iconItem.selected + + /** + * @brief This property sets whether or not to reserve space for the icon, even if there is no icon. + * @image html BasicListItemReserve.svg "Left: reserveSpaceForIcon: false. Right: reserveSpaceForIcon: true" width=50% + * @property bool reserveSpaceForIcon + */ + property alias reserveSpaceForIcon: iconItem.visible + + /** + * @brief This property sets whether or not the label of the list item should fill width. + * + * Setting this to false is useful if you have other items in the list item + * that should fill width instead of the label. + * + * @property bool reserveSpaceForLabel + */ + property alias reserveSpaceForLabel: labelItem.visible + + /** + * @brief This property holds whether the list item's height should account for + * the presence of a subtitle. + * + * default: ``false`` + * + * @since 5.77 + * @since org.kde.kirigami 2.15 + */ + property bool reserveSpaceForSubtitle: false + + /** + * @brief This property holds the spacing between the label row and subtitle row. + * @since 5.83 + * @since org.kde.kirigami 2.15 + * @property real textSpacing + */ + property alias textSpacing: labelColumn.spacing + + /** + * @brief This property holds sets whether to make the icon and labels have a disabled look. + * + * This can be used to tweak whether the content elements are visually active + * while preserving an active appearance for any leading or trailing items. + * + * default: ``false`` + * + * @since 5.83 + * @since org.kde.kirigami 2.15 + */ + property bool fadeContent: false + + /** + * @brief This property holds the label item, for accessing the usual Text properties. + * @property QtQuick.Controls.Label labelItem + * @since 5.84 + * @since org.kde.kirigami 2.16 + */ + property alias labelItem: labelItem + + /** + * @brief This property holds the subtitle item, for accessing the usual Text properties. + * @property QtQuick.Controls.Label subtitleItem + * @since 5.84 + * @since org.kde.kirigami 2.16 + */ + property alias subtitleItem: subtitleItem + + default property alias _basicDefault: layout.data +//END properties + +//BEGIN signal handlers + onLeadingChanged: { + if (!!listItem.leading) { + listItem.leading.parent = contItem + listItem.leading.anchors.left = listItem.leading.parent.left + listItem.leading.anchors.top = listItem.leadingFillVertically ? listItem.leading.parent.top : undefined + listItem.leading.anchors.bottom = listItem.leadingFillVertically ? listItem.leading.parent.bottom : undefined + listItem.leading.anchors.verticalCenter = listItem.leadingFillVertically ? undefined : listItem.leading.parent.verticalCenter + layout.anchors.left = listItem.leading.right + layout.anchors.leftMargin = Qt.binding(function() { return listItem.leadingPadding }) + } else { + layout.anchors.left = contentItem.left + layout.anchors.leftMargin = 0 + } + } + + onTrailingChanged: { + if (!!listItem.trailing) { + listItem.trailing.parent = contItem + listItem.trailing.anchors.right = listItem.trailing.parent.right + listItem.trailing.anchors.top = listItem.trailingFillVertically ? listItem.trailing.parent.top : undefined + listItem.trailing.anchors.bottom = listItem.trailingFillVertically ? listItem.trailing.parent.bottom : undefined + listItem.trailing.anchors.verticalCenter = listItem.trailingFillVertically ? undefined : listItem.trailing.parent.verticalCenter + layout.anchors.right = listItem.trailing.left + layout.anchors.rightMargin = Qt.binding(function() { return listItem.trailingPadding }) + } else { + layout.anchors.right = contentItem.right + layout.anchors.rightMargin = 0 + } + } + + Keys.onEnterPressed: action ? action.trigger() : clicked() + Keys.onReturnPressed: action ? action.trigger() : clicked() +//END signal handlers + + icon: action ? action.icon.name || action.icon.source : undefined + + contentItem: Item { + id: contItem + implicitWidth: (listItem.leading || {implicitWidth: 0}).implicitWidth + layout.implicitWidth + (listItem.trailing || {implicitWidth: 0}).implicitWidth + Binding on implicitHeight { + value: Math.max(iconItem.size, (!subtitleItem.visible && listItem.reserveSpaceForSubtitle ? (labelItem.implicitHeight + labelColumn.spacing + subtitleItem.implicitHeight): labelColumn.implicitHeight) ) + delayed: true + } + + RowLayout { + id: layout + spacing: LayoutMirroring.enabled ? listItem.rightPadding : listItem.leftPadding + anchors.left: contItem.left + anchors.leftMargin: listItem.leading ? listItem.leadingPadding : 0 + anchors.right: contItem.right + anchors.rightMargin: listItem.trailing ? listItem.trailingPadding : 0 + anchors.verticalCenter: parent.verticalCenter + + Icon { + id: iconItem + source: { + if (!listItem.icon) { + return undefined + } + if (listItem.icon.hasOwnProperty) { + if (listItem.icon.hasOwnProperty("name") && listItem.icon.name !== "") + return listItem.icon.name; + if (listItem.icon.hasOwnProperty("source")) + return listItem.icon.source; + } + return listItem.icon; + } + property int size: subtitleItem.visible || reserveSpaceForSubtitle ? Units.iconSizes.medium : Units.iconSizes.smallMedium + Layout.minimumHeight: size + Layout.maximumHeight: size + Layout.minimumWidth: size + Layout.maximumWidth: size + selected: (listItem.highlighted || listItem.checked || (listItem.pressed && listItem.supportsMouseEvents)) + opacity: listItem.fadeContent ? 0.4 : 1.0 + visible: source !== undefined + } + ColumnLayout { + id: labelColumn + spacing: 0 + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + QQC2.Label { + id: labelItem + text: listItem.text + Layout.fillWidth: true + Layout.alignment: subtitleItem.visible ? Qt.AlignLeft | Qt.AlignBottom : Qt.AlignLeft | Qt.AlignVCenter + color: (listItem.highlighted || listItem.checked || (listItem.pressed && listItem.supportsMouseEvents)) ? listItem.activeTextColor : listItem.textColor + elide: Text.ElideRight + font.weight: listItem.bold ? Font.Bold : Font.Normal + opacity: listItem.fadeContent ? 0.4 : 1.0 + } + QQC2.Label { + id: subtitleItem + Layout.fillWidth: true + Layout.alignment: subtitleItem.visible ? Qt.AlignLeft | Qt.AlignTop : Qt.AlignLeft | Qt.AlignVCenter + color: (listItem.highlighted || listItem.checked || (listItem.pressed && listItem.supportsMouseEvents)) ? listItem.activeTextColor : listItem.textColor + elide: Text.ElideRight + font: Theme.smallFont + opacity: listItem.bold + ? (listItem.fadeContent ? 0.3 : 0.9) + : (listItem.fadeContent ? 0.1 : 0.7) + visible: text.length > 0 + } + } + } + } +} diff --git a/src/controls/Card.qml b/src/controls/Card.qml new file mode 100644 index 0000000..fa0f790 --- /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 2.6 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.0 as Controls +import org.kde.kirigami 2.12 as Kirigami +import "private" + +/** + * @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 Card::actions + */ + property list actions + + /** + * @brief This property holds hidden actions that will be available in the footer. + * + * These actions will only be shown in the overflow menu, even when there is enough space. + * + * @deprecated Use actions with a ``Kirigami.DisplayHint.AlwaysHide`` as displayHint. + * @see org::kde::kirigami::DisplayHint + * @property list hiddenActions + * @since 2.6 + */ + property alias hiddenActions: actionsToolBar.hiddenActions + + /** + * @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 + + + header: BannerImage { + id: bannerImage + anchors.leftMargin: -root.leftPadding + root.background.border.width + anchors.topMargin: -root.topPadding + root.background.border.width + anchors.rightMargin: root.headerOrientation === Qt.Vertical ? -root.rightPadding + root.background.border.width : 0 + anchors.bottomMargin: root.headerOrientation === Qt.Horizontal ? -root.bottomPadding + root.background.border.width : 0 + //height: Layout.preferredHeight + implicitWidth: root.headerOrientation === Qt.Horizontal ? sourceSize.width : Layout.preferredWidth + Layout.preferredHeight: (source.toString() !== "" ? width / (sourceSize.width / sourceSize.height) : Layout.minimumHeight) + anchors.topMargin + anchors.bottomMargin + + 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: (root.headerOrientation === Qt.Horizontal && widthWithBorder < root.width) ? 0 : radiusFromBackground + corners.bottomLeftRadius: (root.headerOrientation !== Qt.Horizontal && heightWithBorder < root.height) ? 0 : radiusFromBackground + corners.bottomRightRadius: heightWithBorder < root.height ? 0 : radiusFromBackground + } + + onHeaderChanged: { + if (!header) { + return; + } + + header.anchors.leftMargin = Qt.binding(() => -root.leftPadding); + header.anchors.topMargin = Qt.binding(() => -root.topPadding); + header.anchors.rightMargin = Qt.binding(() => root.headerOrientation === Qt.Vertical ? -root.rightPadding : 0); + header.anchors.bottomMargin = Qt.binding(() => root.headerOrientation === Qt.Horizontal ? -root.bottomPadding : 0); + } + + footer: Kirigami.ActionToolBar { + id: actionsToolBar + actions: root.actions + position: Controls.ToolBar.Footer + visible: root.footer === actionsToolBar + } +} diff --git a/src/controls/CardsGridView.qml b/src/controls/CardsGridView.qml new file mode 100644 index 0000000..e4bb934 --- /dev/null +++ b/src/controls/CardsGridView.qml @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.10 +import QtQuick.Controls 2.0 as Controls +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.4 as Kirigami +import "private" + +/** + * @brief CardsGridView is used to display a grid of Cards generated from any model. + * + * The behavior is same as CardsLayout, and it allows cards to be put in one or two + * columns depending on the available width. + * + * GridView has the limitation that every Card must have the same exact height, + * so cellHeight must be manually set to a value in which the content fits + * for every item. + * + * If possible use cards only when you don't need to instantiate a lot + * and use CardsLayout instead. + * + * @see CardsLayout + * @see CardsListView + * @inherit QtQuick.GridView + * @since 2.4 + */ +CardsGridViewPrivate { + id: root + + /** + * @brief This property sets whether the view should fill the first row with columns + * even when there is not enough space. + * + * Set this to true if you want to stop the view from filling the first row with columns, + * even when delegates can't even fill the first row. + * + * default: ``true`` + */ + property bool extraColumns: true + + /** + * @brief This property holds the number of columns the gridview has. + * @since 2.5 + */ + readonly property int columns: { + var maxColumns = maximumColumns > 0 ? maximumColumns : Infinity + var minFromWidth = Math.floor(width / minimumColumnWidth) + var maxFromWidth = Math.ceil(width / maximumColumnWidth) + var extraCount = extraColumns ? Infinity : count + return Math.max(1,Math.min(maxColumns,minFromWidth,maxFromWidth,extraCount)) + } + + /** + * @brief This property holds the maximum number of columns the gridview may have. + * + * default: ``Infinity`` + * + * @since 2.5 + */ + property int maximumColumns: Infinity + + /** + * @brief This property holds the maximum width that the columns may have. + * + * The cards will never become wider than this size; when the GridView 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 that the columns may have. + * + * The cards will never become thinner than this. + * + * 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: ``12 * Kirigami.Units.gridUnit`` + * + * @since 2.5 + */ + property int minimumColumnWidth: Kirigami.Units.gridUnit * 12 + + cellWidth: Math.floor(width/columns) + cellHeight: Math.max(Kirigami.Units.gridUnit * 15, Math.min(cellWidth, maximumColumnWidth) / 1.2) + + /** + * @brief This property holds the delegate of the CardsGridView. + * @see QtQuick.GridView::delegate + */ + default property alias delegate: root._delegateComponent + + topMargin: Kirigami.Units.largeSpacing * 2 + + Keys.onPressed: { + 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/CardsLayout.qml b/src/controls/CardsLayout.qml new file mode 100644 index 0000000..4df0b32 --- /dev/null +++ b/src/controls/CardsLayout.qml @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.6 +import QtQuick.Controls 2.0 as Controls +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.4 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 (In the case of a big model + * CardsListView or CardsGridview should be used instead). + * + * 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 * columns + columnSpacing: Kirigami.Units.largeSpacing * columns + + + // 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 (var i = 0; i < children.length; ++i) { + children[i].Layout.fillHeight = true; + } + } +} diff --git a/src/controls/CardsListView.qml b/src/controls/CardsListView.qml new file mode 100644 index 0000000..9b72823 --- /dev/null +++ b/src/controls/CardsListView.qml @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.0 as Controls +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.4 as Kirigami +import "private" +/** + * 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, unless it has + * headerOrientation set to ``Qt.Horizontal``. + * + * The choice between using this view with AbstractCard or a normal ListView + * with AbstractListItem/BasicListItem 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: { + 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/CheckableListItem.qml b/src/controls/CheckableListItem.qml new file mode 100644 index 0000000..a542229 --- /dev/null +++ b/src/controls/CheckableListItem.qml @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2020 Nate Graham + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.0 as QQC2 +import org.kde.kirigami 2.14 as Kirigami + +/** + * A simple subclass of BasicListItem that adds a checkbox on the left side of + * the layout. The list item's own checked: property controls the check state + * of the checkbox. + * + * When the list item or its checkbox is clicked, the QQC2 action specified in + * the list item's actions: property will be triggered. + * + * @note Due to the way BasicListItem works, the QQC2 action MUST contain the + * line "checked = !checked" as the first line within its "onTriggered:" handler. + * + * Example usage: + * @code{.qml} + * import org.kde.kirigami 2.14 as Kirigami + * + * ListView { + * id: listView + * model: [...] + * delegate: Kirigami.CheckableListItem { + * label: model.display + * + * checked: model.checked + * + * action: Action { + * onTriggered: { + * checked = !checked + * [ do something amazing ] + * } + * } + * } + * } + * @endcode + * @since 2.14 + * @inherit org::kde::kirigami::BasicListItem + */ +Kirigami.BasicListItem { + id: checkableListItem + + checkable: true + activeBackgroundColor: "transparent" + activeTextColor: Kirigami.Theme.textColor + iconSelected: false + + leading: QQC2.CheckBox { + checked: checkableListItem.checked + onToggled: { + checkableListItem.checked = !checkableListItem.checked + + // TODO(Qt6): rephrase as `checkableListItem.action?.trigger();` + if (checkableListItem.action) { + checkableListItem.action.trigger(); + } + } + } +} diff --git a/src/controls/Chip.qml b/src/controls/Chip.qml new file mode 100644 index 0000000..b52c9cd --- /dev/null +++ b/src/controls/Chip.qml @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2022 Felipe Kinoshita +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.19 as Kirigami + +/** + * @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 2.19 as Kirigami + * + * Flow { + * Repeater { + * model: chipsModel + * + * Kirigami.Chip { + * text: model.text + * icon.name: "tag-symbolic" + * closable: model.closable + * onClicked: { + * [...] + * } + * onRemoved: { + * [...] + * } + * } + * } + * } + * @endcode + * + * @inherit org::kde::kirigami::AbstractChip + * @since 2.19 + */ +Kirigami.AbstractChip { + id: chip + + implicitWidth: layout.implicitWidth + implicitHeight: toolButton.implicitHeight + + checkable: !closable + + /** + * @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 + 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() + } + } +} diff --git a/src/controls/ContextDrawer.qml b/src/controls/ContextDrawer.qml new file mode 100644 index 0000000..014deb7 --- /dev/null +++ b/src/controls/ContextDrawer.qml @@ -0,0 +1,197 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 as QQC2 +import org.kde.kirigami 2.4 + +import "private" +import "templates/private" + +/** + * 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 2.4 as Kirigami + * + * Kirigami.ApplicationWindow { + * [...] + * contextDrawer: Kirigami.ContextDrawer { + * id: contextDrawer + * } + * [...] + * } + * @endcode + * + * @code + * import org.kde.kirigami 2.4 as Kirigami + * + * Kirigami.Page { + * [...] + * contextualActions: [ + * 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 + */ +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") + + /** + * This can be any type of object that a ListView can accept as model. + * It expects items compatible with either QtQuick.Action or Kirigami.Action + * + * @see QtQuick.Action + * @see org::kde::kirigami::Action + * @property list actions + */ + property var actions: page ? page.contextualActions : [] + + /** + * @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 + + property Page page: { + if (applicationWindow().pageStack.layers && applicationWindow().pageStack.layers.depth > 1 && applicationWindow().pageStack.layers.currentItem.hasOwnProperty("contextualActions")) { + return applicationWindow().pageStack.layers.currentItem; + } + else if ((applicationWindow().pageStack.currentItem || {}).hasOwnProperty("contextualActions")) { + return applicationWindow().pageStack.currentItem; + } + else { + return applicationWindow().pageStack.lastVisibleItem; + } + } + + // Disable for empty menus or when we have a global toolbar + enabled: menu.count > 0 && + (typeof applicationWindow() === "undefined" || !applicationWindow().pageStack.globalToolBar || + (applicationWindow().pageStack.lastVisibleItem && applicationWindow().pageStack.lastVisibleItem.globalToolBarStyle !== ApplicationHeaderStyle.ToolBar) || + (applicationWindow().pageStack.layers && applicationWindow().pageStack.layers.depth > 1 && applicationWindow().pageStack.layers.currentItem && applicationWindow().pageStack.layers.currentItem.globalToolBarStyle !== ApplicationHeaderStyle.ToolBar)) + edge: Qt.application.layoutDirection === Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge + drawerOpen: false + + // list items go to edges, have their own padding + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + + handleVisible: applicationWindow === undefined ? false : applicationWindow().controlsVisible + + onPeekingChanged: { + if (page) { + page.contextualActionsAboutToShow(); + } + } + contentItem: QQC2.ScrollView { + // this just to create the attached property + Theme.inherit: true + implicitWidth: Units.gridUnit * 20 + ListView { + id: menu + interactive: contentHeight > height + model: { + if (typeof root.actions === "undefined") { + return null; + } + if (root.actions.length === 0) { + return null; + } else { + + // Check if at least one action is visible + var somethingVisible = false; + for (var i=0; i 0 ? menu.height - menu.contentHeight : 0 + header: Item { + height: heading.height + width: menu.width + Heading { + id: heading + anchors { + left: parent.left + right: parent.right + margins: Units.largeSpacing + } + elide: Text.ElideRight + level: 2 + text: root.title + } + } + delegate: Column { + width: parent.width + ContextDrawerActionItem { + width: parent.width + } + Repeater { + model: modelData.hasOwnProperty("expandible") && modelData.expandible ? modelData.children : null + delegate: ContextDrawerActionItem { + width: parent.width + leftPadding: Units.largeSpacing * 2 + opacity: !root.collapsed + } + } + } + } + } +} diff --git a/src/controls/Dialog.qml b/src/controls/Dialog.qml new file mode 100644 index 0000000..92ca9a7 --- /dev/null +++ b/src/controls/Dialog.qml @@ -0,0 +1,521 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + SPDX-FileCopyrightText: 2021 Noah Davis + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.2 +import QtQuick.Templates 2.15 as T +import QtQuick.Controls 2.15 as Controls +import org.kde.kirigami 2.12 as Kirigami +import QtGraphicalEffects 1.12 +import "templates/private" as Private + +/** + * @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 2.15 + * import QtQuick.Layouts 1.2 + * import QtQuick.Controls 2.15 as Controls + * import org.kde.kirigami 2.19 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: Controls.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} + * 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: Controls.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 + + // TODO KF6: remove this property + /** + * @deprecated This property will be removed in the next major frameworks release (KF6) + */ + property Item mainItem: contentControl.contentChildren.length > 0 ? contentControl.contentChildren[0] : null + + /** + * @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.height - 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.width - Kirigami.Units.largeSpacing * 2 + + /** + * @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 2.15 + * import QtQuick.Controls 2.15 as Controls + * import org.kde.kirigami 2.18 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") + * iconName: "media-playback-start" + * onTriggered: { + * //... + * dialog.close(); + * } + * } + * ] + * } + * @endcode + * + * @see org::kde::kirigami::Action + */ + property list customFooterActions + + // default standard button + standardButtons: Controls.Dialog.Close + + 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; + } + } + + // calculate dimensions + implicitWidth: contentItem.implicitWidth + leftPadding + rightPadding // maximum width enforced from our content (one source of truth) to avoid binding loops + implicitHeight: contentItem.implicitHeight + topPadding + bottomPadding + + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0) + + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0); + + // misc. dialog settings + closePolicy: Controls.Popup.CloseOnEscape | Controls.Popup.CloseOnReleaseOutside + modal: true + clip: false + padding: 0 + + // 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: Math.round((parent.width - width) / 2) + y: Math.round((parent.height - height) / 2) + Kirigami.Units.gridUnit * 2 * (1 - opacity) // 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 + Controls.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: Item { + RectangularGlow { + anchors.fill: rect + anchors.topMargin: 1 + cornerRadius: rect.radius * 2 + glowRadius: 2 + spread: 0.2 + color: Qt.rgba(0, 0, 0, 0.3) + } + + Rectangle { + id: rect + anchors.fill: parent + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + color: Kirigami.Theme.backgroundColor + radius: Kirigami.Units.smallSpacing + } + } + + // dialog content + contentItem: ColumnLayout { + Controls.ScrollView { + id: contentControl + + // ensure view colour scheme, and background color + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.View + + Controls.ScrollBar.horizontal.policy: Controls.ScrollBar.AlwaysOff + + // height of everything else in the dialog other than the content + property real otherHeights: root.header.height + root.footer.height + 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: (contentChildren.length === 1 && contentChildren[0].implicitWidth > 0 + ? contentChildren[0].implicitWidth + : (contentItem.implicitWidth > 0 ? contentItem.implicitWidth : contentItem.width)) + leftPadding + rightPadding + property real calculatedImplicitHeight: (contentChildren.length === 1 && contentChildren[0].implicitHeight > 0 + ? contentChildren[0].implicitHeight + : (contentItem.implicitHeight > 0 ? contentItem.implicitHeight : contentItem.height)) + topPadding + bottomPadding + + // 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 + Layout.preferredWidth: (root.preferredWidth >= 0 ? root.preferredWidth : calculatedImplicitWidth) + Layout.preferredHeight: root.preferredHeight >= 0 ? root.preferredHeight - otherHeights : calculatedImplicitHeight + + Layout.fillWidth: true + Layout.maximumWidth: calculatedMaximumWidth + Layout.maximumHeight: calculatedMaximumHeight - otherHeights // we enforce maximum height solely from the content + + // 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] + when: contentControl.contentChildren.length === 1 + property: "width" + + // we want to avoid horizontal scrolling, so we apply maximumWidth as a hint if necessary + property real preferredWidthHint: contentControl.contentItem.width + property real maximumWidthHint: contentControl.calculatedMaximumWidth - contentControl.leftPadding - contentControl.rightPadding + + value: Math.min(maximumWidthHint,preferredWidthHint) + } + } + } + + header: T.Control { + 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 + + contentItem: RowLayout { + Kirigami.Heading { + id: heading + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + level: 2 + text: root.title === "" ? " " : root.title // always have text to ensure header height + elide: Text.ElideRight + + // use tooltip for long text that is elided + Controls.ToolTip.visible: truncated && titleHoverHandler.hovered + Controls.ToolTip.text: root.title + HoverHandler { id: titleHoverHandler } + } + Kirigami.Icon { + id: closeIcon + visible: root.showCloseButton + + // 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 + Kirigami.Units.largeSpacing) + Layout.alignment: tallHeader ? Qt.AlignRight | Qt.AlignTop : Qt.AlignRight | Qt.AlignVCenter + Layout.topMargin: tallHeader ? Kirigami.Units.largeSpacing : 0 + implicitHeight: Kirigami.Units.iconSizes.smallMedium + implicitWidth: implicitHeight + + source: closeMouseArea.containsMouse ? "window-close" : "window-close-symbolic" + active: closeMouseArea.containsMouse + MouseArea { + id: closeMouseArea + hoverEnabled: Qt.styleHints.useHoverEffects + anchors.fill: parent + onClicked: root.reject() + } + } + } + + // header background + background: Kirigami.ShadowedRectangle { + corners.topLeftRadius: Kirigami.Units.smallSpacing + corners.topRightRadius: Kirigami.Units.smallSpacing + Kirigami.Theme.colorSet: Kirigami.Theme.Header + Kirigami.Theme.inherit: false + color: Kirigami.Theme.backgroundColor + Kirigami.Separator { + id: headerSeparator + width: parent.width + anchors.bottom: parent.bottom + } + } + } + + // 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: contentItem.implicitHeight === 0 + implicitHeight: bufferMode ? Kirigami.Units.smallSpacing : contentItem.implicitHeight + + leftPadding: 0; rightPadding: 0; bottomPadding: 0 + topPadding: bufferMode ? 0 : footerSeparator.implicitHeight // add space for the separator above the footer + + contentItem: RowLayout { + spacing: parent.spacing + + Loader { + id: leadingLoader + sourceComponent: root.footerLeadingComponent + } + + // footer buttons + Controls.DialogButtonBox { + // we don't explicitly set padding, to let the style choose the padding + id: dialogButtonBox + standardButtons: root.standardButtons + visible: count > 0 + + Layout.fillWidth: true + Layout.alignment: dialogButtonBox.alignment + + position: Controls.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 { + model: root.customFooterActions + // we have to use Button instead of ToolButton, because ToolButton has no visual distinction when disabled + delegate: Controls.Button { + flat: flatFooterButtons + action: modelData + visible: modelData.visible + } + } + } + + Loader { + id: trailingLoader + sourceComponent: root.footerTrailingComponent + } + } + + background: Kirigami.ShadowedRectangle { + // curved footer bottom corners + corners.bottomLeftRadius: Kirigami.Units.smallSpacing + corners.bottomRightRadius: Kirigami.Units.smallSpacing + + // we act as a content buffer if nothing is in the footer + Kirigami.Theme.colorSet: footerToolBar.bufferMode ? Kirigami.Theme.View : Kirigami.Theme.Window + Kirigami.Theme.inherit: false + color: Kirigami.Theme.backgroundColor + + // separator above footer + Kirigami.Separator { + id: footerSeparator + visible: !footerToolBar.bufferMode + width: parent.width + anchors.top: parent.top + } + } + } +} diff --git a/src/controls/FlexColumn.qml b/src/controls/FlexColumn.qml new file mode 100644 index 0000000..bc43a61 --- /dev/null +++ b/src/controls/FlexColumn.qml @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import org.kde.kirigami 2.13 as Kirigami + +/** + * @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 + + Layout.fillWidth: true + Layout.fillHeight: true + + enum CrossAxis { + Left, + Center, + Right + } + + ColumnLayout { + id: __inner + 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/FormLayout.qml b/src/controls/FormLayout.qml new file mode 100644 index 0000000..8aea22e --- /dev/null +++ b/src/controls/FormLayout.qml @@ -0,0 +1,465 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 + +import org.kde.kirigami 2.18 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 org.kde.kirigami 2.3 as Kirigami + * Kirigami.FormLayout { + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * Kirigami.Separator { + * Kirigami.FormData.label: "Section Title" + * Kirigami.FormData.isSection: true + * } + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * 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 (let i in twinFormLayouts) { + if (!(root in twinFormLayouts[i].children[0].reverseTwins)) { + twinFormLayouts[i].children[0].reverseTwins.push(root) + Qt.callLater(() => twinFormLayouts[i].children[0].reverseTwinsChanged()); + } + } + } + + Component.onCompleted: { + relayoutTimer.triggered() + } + + Component.onDestruction: { + for (let i in twinFormLayouts) { + const twin = twinFormLayouts[i]; + const child = twin.children[0]; + child.reverseTwins = child.reverseTwins.filter((value, index, arr) => 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.smallSpacing + width: root.wideMode ? undefined : root.width + 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 (let i in knownItems) { + let actualWidth = knownItems[i].implicitWidth + if (knownItems[i].Layout.preferredWidth > 0) { + actualWidth = knownItems[i].Layout.preferredWidth + } + actualWidth = Math.min(actualWidth, knownItems[i].Layout.maximumWidth) + actualWidth = Math.max(actualWidth, knownItems[i].Layout.minimumWidth) + + hint = Math.max(hint, actualWidth); + } + return hint; + } + property int buddiesImplicitWidth: { + let hint = 0; + + // HACK: we use var instead of let here, since it seems to trigger a very obscure bug + // see: https://invent.kde.org/teams/plasma-mobile/issues/-/issues/88 + for (var i in buddies) { + if (buddies[i].visible && !buddies[i].item.Kirigami.FormData.isSection) { + hint = Math.max(hint, buddies[i].implicitWidth); + } + } + return hint; + } + readonly property var actualTwinFormLayouts: { + // We need to copy that array by value + const list = lay.reverseTwins.slice(); + for (let i in twinFormLayouts) { + let parentLay = twinFormLayouts[i]; + if (!parentLay || !parentLay.hasOwnProperty("children")) { + continue; + } + list.push(parentLay); + for (let j in parentLay.children[0].reverseTwins) { + let childLay = parentLay.children[0].reverseTwins[j]; + 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 (let i in lay.actualTwinFormLayouts) { + if (lay.actualTwinFormLayouts[i] && lay.actualTwinFormLayouts[i].hasOwnProperty("children")) { + hint = Math.max(hint, lay.actualTwinFormLayouts[i].children[0].buddiesImplicitWidth); + } + } + return hint; + } + Layout.preferredHeight:2 + } + Item { + Layout.preferredWidth: { + let hint = Math.min(root.width, lay.knownItemsImplicitWidth); + for (let i in lay.actualTwinFormLayouts) { + if (lay.actualTwinFormLayouts[i] && lay.actualTwinFormLayouts[i].hasOwnProperty("children")) { + hint = Math.max(hint, lay.actualTwinFormLayouts[i].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) { + 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) { + if (root.wideMode) { + return item.Kirigami.FormData.labelAlignment !== 0 ? item.Kirigami.FormData.labelAlignment : Text.AlignVCenter + } + return Text.AlignBottom + } + } + + Timer { + id: relayoutTimer + interval: 0 + onTriggered: { + let __items = 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: item}) + + // if section, label goes after the separator + if (item.Kirigami.FormData.isSection) { + // put an extra spacer + var placeHolder = placeHolderComponent.createObject(lay, {item: item}); + itemContainer.parent = lay; + } + + let buddy; + if (item.Kirigami.FormData.checkable) { + buddy = checkableBuddyComponent.createObject(lay, {item: item}) + } else { + buddy = buddyComponent.createObject(lay, {item: 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 var item + enabled: item.enabled + visible: item.visible + + // 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: Math.max(item.implicitWidth, 1) + implicitHeight: Math.max(item.implicitHeight, 1) + Layout.preferredWidth: Math.max(1, item.Layout.preferredWidth > 0 ? item.Layout.preferredWidth : Math.ceil(item.implicitWidth)) + Layout.preferredHeight: Math.max(1, item.Layout.preferredHeight > 0 ? item.Layout.preferredHeight : Math.ceil(item.implicitHeight)) + + Layout.minimumWidth: item.Layout.minimumWidth + Layout.minimumHeight: item.Layout.minimumHeight + + Layout.maximumWidth: item.Layout.maximumWidth + Layout.maximumHeight: item.Layout.maximumHeight + + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillWidth: 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: item.x = x + lay.x; + // Assume lay.y is always 0 + onYChanged: item.y = y + lay.y; + onWidthChanged: item.width = width; + Component.onCompleted: item.x = x + lay.x; + Connections { + target: lay + function onXChanged() { item.x = x + lay.x } + } + } + } + Component { + id: placeHolderComponent + Item { + property var item + enabled: item.enabled + visible: item.visible + width: Kirigami.Units.smallSpacing + height: Kirigami.Units.smallSpacing + Layout.topMargin: item.height > 0 ? Kirigami.Units.smallSpacing : 0 + onItemChanged: { + if (!item) { + labelItem.destroy(); + } + } + } + } + Component { + id: buddyComponent + Kirigami.Heading { + id: labelItem + + property Item item + property int index + enabled: item.enabled && item.Kirigami.FormData.enabled + visible: item.visible && (root.wideMode || text.length > 0) + Kirigami.MnemonicData.enabled: item.Kirigami.FormData.buddyFor && item.Kirigami.FormData.buddyFor.activeFocusOnTab + Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.FormLabel + Kirigami.MnemonicData.label: item.Kirigami.FormData.label + text: Kirigami.MnemonicData.richTextLabel + 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.Kirigami.FormData.label.length > 0) { + if (root.wideMode && !(item.Kirigami.FormData.buddyFor instanceof TextArea)) { + 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 (root.wideMode && item.Kirigami.FormData.buddyFor.parent !== root) { + return item.Kirigami.FormData.buddyFor.y; + } + if (root.wideMode && (item.Kirigami.FormData.buddyFor instanceof TextArea)) { + return Kirigami.Units.smallSpacing; + } + if (index === 0 || root.wideMode) { + return 0; + } + return Kirigami.Units.largeSpacing * 2; + } + onItemChanged: { + if (!item) { + labelItem.destroy(); + } + } + Shortcut { + sequence: labelItem.Kirigami.MnemonicData.sequence + onActivated: item.Kirigami.FormData.buddyFor.forceActiveFocus() + } + } + } + Component { + id: checkableBuddyComponent + CheckBox { + id: labelItem + property Item item + visible: item.visible + Kirigami.MnemonicData.enabled: item.Kirigami.FormData.buddyFor && item.Kirigami.FormData.buddyFor.activeFocusOnTab + Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.FormLabel + Kirigami.MnemonicData.label: item.Kirigami.FormData.label + + Layout.columnSpan: item.Kirigami.FormData.isSection ? lay.columns : 1 + Layout.preferredHeight: { + if (item.Kirigami.FormData.label.length > 0) { + if (root.wideMode && !(item.Kirigami.FormData.buddyFor instanceof TextArea)) { + return Math.max(implicitHeight, item.Kirigami.FormData.buddyFor.height) + } + return implicitHeight + } + return Kirigami.Units.smallSpacing; + } + + Layout.alignment: temp.effectiveLayout(this) + Layout.topMargin: item.Kirigami.FormData.buddyFor.height > implicitHeight * 2 ? Kirigami.Units.smallSpacing/2 : 0 + + activeFocusOnTab: indicator.visible && indicator.enabled + // HACK: desktop style checkboxes have also the text in the background item + // text: labelItem.Kirigami.MnemonicData.richTextLabel + enabled: labelItem.item.Kirigami.FormData.enabled + checked: labelItem.item.Kirigami.FormData.checked + + onItemChanged: { + if (!item) { + labelItem.destroy(); + } + } + Shortcut { + sequence: labelItem.Kirigami.MnemonicData.sequence + onActivated: { + checked = !checked + item.Kirigami.FormData.buddyFor.forceActiveFocus() + } + } + onCheckedChanged: { + item.Kirigami.FormData.checked = checked + } + contentItem: Kirigami.Heading { + id: labelItemHeading + level: labelItem.item.Kirigami.FormData.isSection ? 3 : 5 + text: labelItem.Kirigami.MnemonicData.richTextLabel + type: item.Kirigami.FormData.isSection ? Kirigami.Heading.Type.Primary : Kirigami.Heading.Type.Normal + verticalAlignment: temp.effectiveTextLayout(labelItem.item) + enabled: labelItem.item.Kirigami.FormData.enabled + leftPadding: height // parent.indicator.width + } + Rectangle { + enabled: labelItem.indicator.enabled + anchors.left: labelItemHeading.left + anchors.right: labelItemHeading.right + anchors.top: labelItemHeading.bottom + anchors.leftMargin: labelItemHeading.leftPadding + height: 1 + color: Kirigami.Theme.highlightColor + visible: labelItem.activeFocus && labelItem.indicator.visible + } + } + } +} diff --git a/src/controls/GlobalDrawer.qml b/src/controls/GlobalDrawer.qml new file mode 100644 index 0000000..d396ab4 --- /dev/null +++ b/src/controls/GlobalDrawer.qml @@ -0,0 +1,601 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Templates 2.3 as T2 +import QtQuick.Controls 2.2 as QQC2 +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.13 + +import "private" +import "templates/private" + +/** + * 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 2.4 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 + * + */ +OverlayDrawer { + id: root + edge: Qt.application.layoutDirection === Qt.RightToLeft ? Qt.RightEdge : Qt.LeftEdge + handleClosedIcon.source: null + handleOpenIcon.source: null + handleVisible: (modal || !drawerOpen) && (typeof(applicationWindow)===typeof(Function) && applicationWindow() ? applicationWindow().controlsVisible : true) && (!isMenu || Settings.isMobile) + interactive: Settings.hasTransientTouchInput || Settings.isMobile + + enabled: !isMenu || 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 alias title: bannerImage.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 alias titleIcon: bannerImage.titleIcon + + /** + * @brief This property holds the banner image source. + * @see org::kde::kirigami::ShadowedImage::source + * @property url bannerImageSource + */ + property alias bannerImageSource: bannerImage.source + + /** + * @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 2.4 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 Item header + + /** + * @brief This property sets drawers banner visibility. + * + * If true, the banner area (which can contain an image, + * an icon, and a title) will be visible. + * + * default: `the banner will be visible only on mobile platforms` + * + * @since 2.12 + */ + property bool bannerVisible: Settings.isMobile + + /** + * @brief This property holds items that are displayed above the actions. + * + * Example usage: + * @code + * import org.kde.kirigami 2.4 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 2.4 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 Action currentSubMenu: stackView.currentItem ? 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 signal notifies that the banner has been clicked. + */ + signal bannerClicked() + + /** + * @brief This function reverts the menu back to its initial state + */ + function resetMenu() { + stackView.pop(stackView.get(0, T2.StackView.DontLoad)); + if (root.modal) { + root.drawerOpen = false; + } + } + + // rightPadding: !Settings.isMobile && mainFlickable.contentHeight > mainFlickable.height ? Units.gridUnit : Units.smallSpacing + + Theme.colorSet: modal ? Theme.Window : Theme.View + + onHeaderChanged: { + if (header) { + header.parent = headerContainer + header.Layout.fillWidth = true; + if (header.z === undefined) { + header.z = 1; + } + if (header instanceof T2.ToolBar) { + header.position = T2.ToolBar.Header + } else if (header instanceof T2.TabBar) { + header.position = T2.TabBar.Header + } else if (header instanceof T2.DialogButtonBox) { + header.position = T2.DialogButtonBox.Header + } + } + } + + contentItem: QQC2.ScrollView { + id: scrollView + //ensure the attached property exists + Theme.inherit: true + anchors.fill: parent + implicitWidth: Math.min (Units.gridUnit * 20, root.parent.width * 0.8) + QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff + QQC2.ScrollBar.vertical.anchors { + top: scrollView.top + bottom: scrollView.bottom + topMargin: headerParent.height + headerParent.y + } + + Flickable { + id: mainFlickable + contentWidth: width + contentHeight: mainColumn.Layout.minimumHeight + topMargin: headerParent.height + + ColumnLayout { + id: headerParent + parent: mainFlickable + anchors { + left: parent.left + right: parent.right + rightMargin: Math.min(0, -scrollView.width + mainFlickable.width) + } + spacing: 0 + y: bannerImage.visible ? Math.max(headerContainer.height, -mainFlickable.contentY) - height : 0 + + Layout.fillWidth: true + // visible: !bannerImage.empty || root.collapsible + + BannerImage { + id: bannerImage + + + visible: !bannerImage.empty && opacity > 0 && root.bannerVisible + opacity: !root.collapsed + fillMode: Image.PreserveAspectCrop + + Behavior on opacity { + OpacityAnimator { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + // leftPadding: root.collapsible ? collapseButton.width + Units.smallSpacing*2 : topPadding + MouseArea { + anchors.fill: parent + onClicked: root.bannerClicked() + } + EdgeShadow { + edge: Qt.BottomEdge + visible: bannerImageSource != "" + anchors { + left: parent.left + right: parent.right + bottom: parent.top + } + } + } + RowLayout { + id: headerContainer + Theme.inherit: false + Theme.colorSet: Theme.Window + + Layout.fillWidth: true + visible: contentItem && opacity > 0 + // Workaround for https://bugreports.qt.io/browse/QTBUG-90034 + Layout.preferredHeight: implicitHeight <= 0 || opacity === 1 ? -1 : implicitHeight * opacity + opacity: !root.collapsed || showHeaderWhenCollapsed + Behavior on opacity { + // not an animator as is binded + NumberAnimation { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + } + + + ColumnLayout { + id: mainColumn + width: mainFlickable.width + spacing: 0 + height: Math.max(root.height - headerParent.height, Layout.minimumHeight) + + ColumnLayout { + id: topContent + spacing: 0 + Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: root.leftPadding + Layout.rightMargin: root.rightPadding + Layout.bottomMargin: 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: Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + + T2.StackView { + id: stackView + clip: true + Layout.fillWidth: true + Layout.minimumHeight: currentItem ? currentItem.implicitHeight : 0 + Layout.maximumHeight: Layout.minimumHeight + property ActionsMenu openSubMenu + 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: Units.veryLongDuration; easing.type: Easing.OutCubic } + } + + popExit: Transition { + NumberAnimation { property: "x"; from: 0; to: (stackView.mirrored ? -1 : 1) * stackView.width; duration: Units.veryLongDuration; easing.type: Easing.OutCubic } + } + + pushEnter: Transition { + NumberAnimation { property: "x"; from: (stackView.mirrored ? -1 : 1) * stackView.width; to: 0; duration: Units.veryLongDuration; easing.type: Easing.OutCubic } + } + + pushExit: Transition { + NumberAnimation { property: "x"; from: 0; to: (stackView.mirrored ? -1 : 1) * -stackView.width; duration: Units.veryLongDuration; easing.type: Easing.OutCubic } + } + + replaceEnter: Transition { + NumberAnimation { property: "x"; from: (stackView.mirrored ? -1 : 1) * stackView.width; to: 0; duration: Units.veryLongDuration; easing.type: Easing.OutCubic } + } + + replaceExit: Transition { + NumberAnimation { property: "x"; from: 0; to: (stackView.mirrored ? -1 : 1) * -stackView.width; duration: Units.veryLongDuration; easing.type: Easing.OutCubic } + } + } + Item { + Layout.fillWidth: true + Layout.fillHeight: root.actions.length>0 + Layout.minimumHeight: 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: Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + Item { + Layout.minimumWidth: Units.smallSpacing + Layout.minimumHeight: root.bottomPadding + } + + Component { + id: menuComponent + + Column { + spacing: 0 + property alias model: actionsRepeater.model + property Action current + + property int level: 0 + Layout.maximumHeight: Layout.minimumHeight + + BasicListItem { + id: backItem + visible: level > 0 + supportsMouseEvents: true + icon: (LayoutMirroring.enabled ? "go-previous-symbolic-rtl" : "go-previous-symbolic") + + label: MnemonicData.richTextLabel + MnemonicData.enabled: backItem.enabled && backItem.visible + MnemonicData.controlType: MnemonicData.MenuItem + MnemonicData.label: qsTr("Back") + + separatorVisible: false + 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.MnemonicData.sequence + onActivated: backItem.clicked() + } + + Repeater { + id: actionsRepeater + + readonly property bool withSections: { + for (var i = 0; i < root.actions.length; i++) { + let action = root.actions[i]; + if (!(action.hasOwnProperty("expandible") && action.expandible)) { + return false; + } + } + return true; + } + + model: root.actions + delegate: Column { + width: parent.width + GlobalDrawerActionItem { + id: drawerItem + visible: (modelData.hasOwnProperty("visible") && modelData.visible) && (root.collapsed || !(modelData.hasOwnProperty("expandible") && modelData.expandible)) + width: parent.width + onCheckedChanged: { + // move every checked item into view + if (checked && topContent.height + backItem.height + (model.index + 1) * height - mainFlickable.contentY > mainFlickable.height) { + mainFlickable.contentY += height + } + } + Theme.colorSet: drawerItem.visible && !root.modal && !root.collapsed && actionsRepeater.withSections ? Theme.Window : parent.Theme.colorSet + backgroundColor: Theme.backgroundColor + } + Item { + id: headerItem + visible: !root.collapsed && (modelData.hasOwnProperty("expandible") && modelData.expandible && !!modelData.children && modelData.children.length > 0) + height: sectionHeader.implicitHeight + width: parent.width + ListSectionHeader { + id: sectionHeader + anchors.fill: parent + Theme.colorSet: root.modal ? Theme.View : Theme.Window + contentItem: RowLayout { + Icon { + property int size: Units.iconSizes.smallMedium + Layout.minimumHeight: size + Layout.maximumHeight: size + Layout.minimumWidth: size + Layout.maximumWidth: size + source: modelData.icon.name || modelData.icon.source + } + Heading { + id: header + level: 4 + text: modelData.text + } + Item { + Layout.fillWidth: true + } + } + } + } + Repeater { + id: __repeater + model: headerItem.visible ? modelData.children : null + delegate: GlobalDrawerActionItem { + width: parent.width + opacity: !root.collapsed + leftPadding: actionsRepeater.withSections && !root.collapsed && !root.modal ? padding * 2 : padding * 4 + } + } + } + } + } + } + + QQC2.ToolButton { + icon.name: root.collapsed ? "view-right-new" : "view-right-close" + Layout.fillWidth: root.collapsed + onClicked: root.collapsed = !root.collapsed + visible: root.collapsible && root.collapseButtonVisible + text: root.collapsed ? "" : qsTr("Close Sidebar") + + QQC2.ToolTip.visible: root.collapsed && hovered + QQC2.ToolTip.text: qsTr("Open Sidebar") + QQC2.ToolTip.delay: Units.toolTipDelay + } + } + } + } +} + diff --git a/src/controls/Heading.qml b/src/controls/Heading.qml new file mode 100644 index 0000000..821eea2 --- /dev/null +++ b/src/controls/Heading.qml @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2012 by Sebastian Kügler + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.0 +import QtQuick.Controls 2.0 as QQC2 +import org.kde.kirigami 2.4 + +/** + * @brief A heading label used for subsections of texts. + * + * The characteristics of the text will be automatically set according to the + * 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 2.4 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 property holds the point size between heading levels. + * + * default: ``0`` + * + * @deprecated + */ + property int step: 0 + + /** + * @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: __headerPointSize(level) + font.weight: type === Heading.Type.Primary ? Font.DemiBold : Font.Normal + + opacity: type === Heading.Type.Secondary ? 0.7 : 1 + + Accessible.role: Accessible.Heading + + // TODO KF6: Remove this public method + function headerPointSize(l) { + console.warn("org.kde.plasma.extras/Heading::headerPointSize() is deprecated. Use font.pointSize directly instead"); + return __headerPointSize(l); + } + + // + // W A R N I N G + // ------------- + // + // This method is not part of the Kirigami API. It exists purely as an + // implementation detail. It may change from version to + // version without notice, or even be removed. + // + // We mean it. + // + function __headerPointSize(level) { + const n = Theme.defaultFont.pointSize; + switch (level) { + case 1: + return n * 1.35 + step; + case 2: + return n * 1.20 + step; + case 3: + return n * 1.15 + step; + case 4: + return n * 1.10 + step; + default: + return n + step; + } + } +} diff --git a/src/controls/Hero.qml b/src/controls/Hero.qml new file mode 100644 index 0000000..cd8d1fd --- /dev/null +++ b/src/controls/Hero.qml @@ -0,0 +1,283 @@ +/* + * SPDX-FileCopyrightText: 2020 Marco Martin + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.14 +import QtQuick.Controls 2.4 as QQC2 +import org.kde.kirigami 2.13 as Kirigami + +/** + * @brief An element that implements a shared element transition, otherwise known as a "hero animation". + */ +Item { + id: root + +//BEGIN properties + /** + * @brief This property holds the item to animate from in the Hero animation. + */ + property Item source + + /** + * @brief This property holds the item to animate to in the Hero animation. + */ + property Item destination + + /** + * @brief This property sets whether the source item will reappear + * in the original position when the Hero animation completes. + * + * default: ``true`` + */ + property bool restore: true + + /** + * Group of properties related to the mask of the object when performing a hero animation. + * This contains the default mask as well as the properties required to create a custom mask. + * + * The default mask of the Hero will transition from a circle to a rectangle on open(), and + * from a rectangle to a circle on close(). + * + * * ``sourceProgress: real`` the progress of the animation, where 0 is the start and 1 is the end. + * * ``destinationProgress: real`` the progress of the animation, where 1 is the start and 0 is the end. + * * ``mask.sourceHeight: real`` the height of the source item. + * * ``mask.sourceWidth: real`` the width of the source item. + * * ``mask.destinationWidth: real`` the width of the destination item. + * * ``mask.destinationHeight: real`` the height of the destination item. + * * ``item: Rectangle`` the item used to mask the Hero during animation. This should bind to the sourceProgress and destinationProgress to change as the animation progresses. + * + */ + readonly property QtObject mask: QtObject { + /** + * @brief This property holds the progress of the animation, + * where 0 is the start and 1 is the end. + */ + readonly property real sourceProgress: sourceEffect.progress + + /** + * @brief This property holds the progress of the animation, + * where 1 is the start and 0 is the end. + */ + readonly property real destinationProgress: destinationEffect.progress + + /** + * @brief This property holds the height of the source item. + */ + readonly property real sourceHeight: sourceEffect.height + + /** + * @brief This property holds the width of the source item. + */ + readonly property real sourceWidth: sourceEffect.width + + /** + * @brief This property holds the width of the destination item. + */ + readonly property real destinationWidth: destinationEffect.width + + /** + * @brief This property holds the height of the destination item. + */ + readonly property real destinationHeight: destinationEffect.height + + /** + * @brief This property holds the item used to mask the Hero during animation. + * + * This should bind to the sourceProgress and destinationProgress to change as the animation progresses. + */ + property Item item: Rectangle { + visible: false + color: "white" + + radius: (width/2) * mask.destinationProgress + width: (mask.sourceWidth * mask.sourceProgress) + (mask.destinationWidth * mask.destinationProgress) + height: (mask.sourceHeight * mask.sourceProgress) + (mask.destinationHeight * mask.destinationProgress) + + layer.enabled: true + layer.smooth: true + } + } + + property alias duration: sourceAni.duration + readonly property QtObject easing: QtObject { + property alias amplitude: sourceAni.easing.amplitude + property alias bezierCurve: sourceAni.easing.bezierCurve + property alias overshoot: sourceAni.easing.overshoot + property alias period: sourceAni.easing.period + property alias type: sourceAni.easing.type + } +//END properties + + function open() { + if (source !== null && destination !== null && !heroAnimation.running) { + heroAnimation.source = source + heroAnimation.destination = destination + heroAnimation.restart() + } + } + function close() { + if (source !== null && destination !== null && !heroAnimation.running) { + // doing a switcheroo simplifies the code + heroAnimation.source = destination + heroAnimation.destination = source + heroAnimation.restart() + } + } + + SequentialAnimation { + id: heroAnimation + + property Item source: Item {} + property Item destination: Item {} + + ScriptAction { + script: { + heroAnimation.source.layer.enabled = true + heroAnimation.source.layer.smooth = true + heroAnimation.destination.layer.enabled = true + heroAnimation.destination.layer.smooth = true + sourceEffect.visible = true + destinationEffect.visible = true + sourceEffect.source = null + sourceEffect.source = heroAnimation.source + destinationEffect.source = null + destinationEffect.source = heroAnimation.destination + heroAnimation.source.opacity = 0 + heroAnimation.destination.opacity = 0 + sourceEffect.parent.visible = true + } + } + ParallelAnimation { + NumberAnimation { + id: sourceAni + + target: sourceEffect + property: "progress" + from: 0 + to: 1 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: destinationEffect + property: "progress" + from: 1 + to: 0 + duration: root.duration + easing.amplitude: root.easing.amplitude + easing.bezierCurve: root.easing.bezierCurve + easing.overshoot: root.easing.overshoot + easing.period: root.easing.period + easing.type: root.easing.type + } + } + ScriptAction { + script: { + sourceEffect.visible = false + destinationEffect.visible = false + heroAnimation.source.layer.enabled = false + heroAnimation.source.layer.smooth = false + heroAnimation.destination.layer.enabled = false + heroAnimation.destination.layer.smooth = false + heroAnimation.destination.opacity = 1 + if (root.restore) { + heroAnimation.source.opacity = 1 + } + sourceEffect.parent.visible = false + } + } + + } + + QtObject { + id: __privateShaderSources + readonly property string vertexShader: ` +uniform highp mat4 qt_Matrix; +attribute highp vec4 qt_Vertex; +attribute highp vec2 qt_MultiTexCoord0; +varying highp vec2 qt_TexCoord0; +uniform highp float startX; +uniform highp float startY; +uniform highp float targetX; +uniform highp float targetY; +uniform highp float scaleWidth; +uniform highp float scaleHeight; +uniform highp float progress; + +highp mat4 morph = mat4(1.0 + (scaleWidth - 1.0) * progress, 0.0, 0.0, startX*(1.0-progress) + targetX*progress, + 0.0, 1.0 + (scaleHeight - 1.0) * progress, 0.0, startY*(1.0-progress) + targetY*progress, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0); + +void main() { + qt_TexCoord0 = qt_MultiTexCoord0; + gl_Position = qt_Matrix * qt_Vertex * morph; +} + ` + } + + ShaderEffect { + id: sourceEffect + x: 0 + y: 0 + parent: heroAnimation.source.QQC2.Overlay.overlay + width: heroAnimation.source.width + height: heroAnimation.source.height + visible: false + property variant source: heroAnimation.source + property real progress: 0 + property real startX: heroAnimation.source.Kirigami.ScenePosition.x / (applicationWindow().width / 2) + property real startY: -heroAnimation.source.Kirigami.ScenePosition.y / (applicationWindow().height / 2) + + property real targetX: scaleWidth - 1 + (heroAnimation.destination.Kirigami.ScenePosition.x * 2) / applicationWindow().width + property real targetY: 1-scaleHeight - (heroAnimation.destination.Kirigami.ScenePosition.y * 2) / applicationWindow().height + property real scaleWidth: heroAnimation.destination.width/heroAnimation.source.width + property real scaleHeight: heroAnimation.destination.height/heroAnimation.source.height + vertexShader: __privateShaderSources.vertexShader + fragmentShader: ` +varying highp vec2 qt_TexCoord0; +uniform sampler2D source; +uniform lowp float qt_Opacity; +uniform lowp float progress; +void main() { + gl_FragColor = texture2D(source, qt_TexCoord0) * qt_Opacity * (1.0 - progress); +} + ` + } + + ShaderEffect { + id: destinationEffect + x: 0 + y: 0 + parent: heroAnimation.destination.QQC2.Overlay.overlay + width: heroAnimation.destination.width + height: heroAnimation.destination.height + visible: false + property variant source: heroAnimation.destination + property real progress: sourceEffect.progress + property real startX: heroAnimation.destination.Kirigami.ScenePosition.x / (applicationWindow().width / 2) + property real startY: -heroAnimation.destination.Kirigami.ScenePosition.y / (applicationWindow().height / 2) + + property real targetX: scaleWidth - 1 + (heroAnimation.source.Kirigami.ScenePosition.x * 2) / applicationWindow().width + property real targetY: 1-scaleHeight - (heroAnimation.source.Kirigami.ScenePosition.y * 2) / applicationWindow().height + property real scaleWidth: heroAnimation.source.width/heroAnimation.destination.width + property real scaleHeight: heroAnimation.source.height/heroAnimation.destination.height + + property variant maskSource: root.mask.item + + vertexShader: __privateShaderSources.vertexShader + fragmentShader: ` +varying highp vec2 qt_TexCoord0; +uniform sampler2D source; +uniform sampler2D maskSource; +uniform lowp float qt_Opacity; +uniform lowp float progress; +void main() { + gl_FragColor = texture2D(source, qt_TexCoord0) * texture2D(maskSource, qt_TexCoord0).a * qt_Opacity * (1.0 - progress); +} + ` + } +} diff --git a/src/controls/InlineMessage.qml b/src/controls/InlineMessage.qml new file mode 100644 index 0000000..b08b126 --- /dev/null +++ b/src/controls/InlineMessage.qml @@ -0,0 +1,104 @@ +/* + * 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 2.7 +import org.kde.kirigami 2.5 as Kirigami +import "private" + +import "templates" as T + +/** + * 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 + * InlineMessage { + * type: Kirigami.MessageType.Error + * + * text: "My error message" + * + * 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 org::kde::kirigami::templates::InlineMessage + * @since 5.45 + */ +T.InlineMessage { + id: root + + background: Rectangle { + id: bgBorderRect + + color: { + if (root.type === Kirigami.MessageType.Positive) { + return Kirigami.Theme.positiveTextColor; + } else if (root.type === Kirigami.MessageType.Warning) { + return Kirigami.Theme.neutralTextColor; + } else if (root.type === Kirigami.MessageType.Error) { + return Kirigami.Theme.negativeTextColor; + } + + return Kirigami.Theme.activeTextColor; + } + + radius: Kirigami.Units.smallSpacing / 2 + + Rectangle { + id: bgFillRect + + anchors.fill: parent + anchors.margins: 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/ItemViewHeader.qml b/src/controls/ItemViewHeader.qml new file mode 100644 index 0000000..e3b5363 --- /dev/null +++ b/src/controls/ItemViewHeader.qml @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import QtQuick.Templates 2.0 as T2 +import QtGraphicalEffects 1.0 +import org.kde.kirigami 2.4 as Kirigami +import "private" + +/** + * An item that can be used as an header for a ListView. + * It will play nice with the margin policies of ScrollablePage and can + * automatically shrink when the list is scrolled, like the behavior + * of list headers in many mobile applications. + * It provides some default content: a title and an optional background image + * @since 2.1 + * @inherit org::kde::kirigami::AbstractItemViewHeader + * @deprecated since 5.97; Don't use ItemViewHeader in your views anymore. + * + * TODO KF6 remove + */ +Kirigami.AbstractItemViewHeader { + id: root + property alias title: heading.text + property alias color: heading.color + + property alias backgroundImage: image + + Component.onCompleted: console.warn( "ItemViewHeader is deprecated (since 5.97): No replacemant is available.", (new Error).stack) + + maximumHeight: (backgroundImage.hasImage ? 10 : 6) * Kirigami.Units.gridUnit - (applicationWindow().header ? applicationWindow().header.height : 0) - bottomPadding + bottomPadding: Kirigami.Units.smallSpacing + leftPadding: Kirigami.Units.smallSpacing + + background: Rectangle { + id: backgroundItem + color: Kirigami.Theme.backgroundColor + Image { + id: image + anchors.fill: parent + readonly property bool hasImage: backgroundImage.status === Image.Ready || backgroundImage.status === Image.Loading + fillMode: Image.PreserveAspectCrop + asynchronous: true + } + EdgeShadow { + edge: root.view.headerPositioning === ListView.InlineHeader ? Qt.BottomEdge : Qt.TopEdge + anchors { + right: parent.right + left: parent.left + top: root.view.headerPositioning === ListView.InlineHeader ? undefined : parent.bottom + bottom: root.view.headerPositioning === ListView.InlineHeader ? parent.top : undefined + } + } + + readonly property Page page: { + var obj = root.view; + while(obj && !obj.hasOwnProperty("title") && !obj.hasOwnProperty("isCurrentPage")) { + obj = obj.parent + } + return obj; + } + Rectangle { + id: rect + color: backgroundItem.page && backgroundItem.page.isCurrentPage ? Kirigami.Theme.highlightColor : Kirigami.Theme.disabledTextColor + height: root.bottomPadding + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + } + + contentItem: Item { + Kirigami.Heading { + id: heading + anchors { + fill: parent + margins: Kirigami.Units.smallSpacing + } + + height: undefined + text: page.title + fontSizeMode: Text.Fit + minimumPointSize: 10 + font.pointSize: 30 + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignBottom + //with an image it needs to be white regardless of system palette + color: root.backgroundImage.hasImage ? "white" : Kirigami.Theme.highlightColor + opacity: 1 + elide: Text.ElideRight + + layer.enabled: root.backgroundImage.hasImage + layer.effect: DropShadow { + horizontalOffset: 0 + verticalOffset: 2 + radius: Kirigami.Units.smallSpacing*2 + samples: 32 + color: Qt.rgba(0, 0, 0, 0.7) + } + } + } +} + diff --git a/src/controls/Label.qml b/src/controls/Label.qml new file mode 100644 index 0000000..6ac5aea --- /dev/null +++ b/src/controls/Label.qml @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2011 by Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Window 2.2 +import org.kde.kirigami 2.4 +import QtQuick.Controls 2.0 as Controls + +/** + * This is a label which uses the current Theme. + * + * The characteristics of the text will be automatically set according to the + * current Theme. If you need a more customized text item use the Text component + * from QtQuick. + * + * You can use all elements of the QML Text component, in particular the "text" + * property to define the label text. + * + * @inherit QtQuick.Templates.Label + * @deprecated use QtQuick.Templates.Label directly, it will be styled appropriately + */ +Controls.Label { + height: Math.round(Math.max(paintedHeight, Units.gridUnit * 1.6)) + verticalAlignment: lineCount > 1 ? Text.AlignTop : Text.AlignVCenter + + activeFocusOnTab: false + + Component.onCompleted: { + console.warn("Kirigami.Label is deprecated. Use QtQuickControls2.Label instead") + } +} diff --git a/src/controls/LinkButton.qml b/src/controls/LinkButton.qml new file mode 100644 index 0000000..130bcc3 --- /dev/null +++ b/src/controls/LinkButton.qml @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.2 +import org.kde.kirigami 2.14 +import QtQuick.Controls 2.1 as QQC2 + +/** + * @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 Action action: null + + /** + * @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 + + Accessible.role: Accessible.Button + Accessible.name: text + Accessible.onPressAction: control.clicked(null) + + text: action ? action.text : "" + enabled: !action || action.enabled + onClicked: if (action) action.trigger() + + font.underline: control.enabled && area.containsMouse + color: enabled ? Theme.linkColor : Theme.textColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + + signal pressed(QtObject mouse) + signal clicked(QtObject mouse) + MouseArea { + id: area + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: control.clicked(mouse) + onPressed: control.pressed(mouse) + } +} diff --git a/src/controls/ListItemDragHandle.qml b/src/controls/ListItemDragHandle.qml new file mode 100644 index 0000000..fa41880 --- /dev/null +++ b/src/controls/ListItemDragHandle.qml @@ -0,0 +1,213 @@ +/* + * SPDX-FileCopyrightText: 2018 by Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.6 +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.4 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 + * ... + * Component { + * id: delegateComponent + * Kirigami.AbstractListItem { + * id: listItem + * contentItem: RowLayout { + * Kirigami.ListItemDragHandle { + * listItem: listItem + * listView: mainList + * onMoveRequested: listModel.move(oldIndex, newIndex, 1) + * } + * Controls.Label { + * text: model.label + * } + * } + * } + * } + * ListView { + * id: mainList + * + * model: ListModel { + * id: listModel + * ListItem { + * lablel: "Item 1" + * } + * ListItem { + * lablel: "Item 2" + * } + * ListItem { + * lablel: "Item 3" + * } + * } + * //this is optional to make list items animated when reordered + * moveDisplaced: Transition { + * YAnimator { + * duration: Kirigami.Units.longDuration + * easing.type: Easing.InOutQuad + * } + * } + * delegate: Kirigami.DelegateRecycler { + * 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 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: 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. + */ + signal dropped() + + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: implicitWidth + + MouseArea { + id: mouseArea + anchors.fill: parent + drag { + target: listItem + axis: Drag.YAxis + minimumY: 0 + } + cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor + + Kirigami.Icon { + id: internal + source: "handle-sort" + property int startY + property int mouseDownY + property Item originalParent + opacity: mouseArea.pressed || (!Kirigami.Settings.tabletMode && listItem.hovered) ? 1 : 0.6 + property int listItemLastY + property bool draggingUp + + function arrangeItem() { + var newIndex = listView.indexAt(1, listView.contentItem.mapFromItem(mouseArea, 0, internal.mouseDownY).y); + + if (newIndex > -1 && ((internal.draggingUp && newIndex < index) || (!internal.draggingUp && newIndex > index))) { + root.moveRequested(index, newIndex); + } + } + + anchors.fill: parent + } + preventStealing: true + + + onPressed: { + 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: { + 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: { + listItem.y = internal.originalParent.mapFromItem(listItem, 0, 0).y; + listItem.parent = internal.originalParent; + dropAnimation.running = true; + scrollTimer.running = false; + root.dropped(); + } + onCanceled: released() + 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..4ee0ed2 --- /dev/null +++ b/src/controls/ListSectionHeader.qml @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2019 Björn Feber + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.10 as Kirigami + +/** + * @brief A section delegate for the primitive ListView component. + * + * It's intended to make all listviews look coherent. + * + * Example usage: + * @code + * import QtQuick 2.5 + * import QtQuick.Controls 2.5 as QQC2 + * + * import org.kde.kirigami 2.10 as Kirigami + * + * ListView { + * [...] + * section.delegate: Kirigami.ListSectionHeader { + * label: section + * + * QQC2.Button { + * text: "Button 1" + * } + * QQC2.Button { + * text: "Button 2" + * } + * } + * [...] + * } + * @endcode + */ +Kirigami.AbstractListItem { + id: listSection + + /** + * @brief This property sets the text of the ListView's section header. + * @property string label + */ + property alias label: listSection.text + + default property alias _contents: rowLayout.data + + backgroundColor: Kirigami.Theme.backgroundColor + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Window + + separatorVisible: false + sectionDelegate: true + hoverEnabled: false + + activeFocusOnTab: false + + contentItem: RowLayout { + id: rowLayout + + Kirigami.Heading { + Layout.fillWidth: rowLayout.children.length === 1 + Layout.alignment: Qt.AlignVCenter + level: 3 + text: listSection.text + } + } +} diff --git a/src/controls/LoadingPlaceholder.qml b/src/controls/LoadingPlaceholder.qml new file mode 100644 index 0000000..6e74a3c --- /dev/null +++ b/src/controls/LoadingPlaceholder.qml @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2022 Felipe Kinoshita +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.19 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/MenuDialog.qml b/src/controls/MenuDialog.qml new file mode 100644 index 0000000..de22f05 --- /dev/null +++ b/src/controls/MenuDialog.qml @@ -0,0 +1,119 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.15 as Controls +import org.kde.kirigami 2.19 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 { + * iconName: "media-playback-start" + * text: i18nc("Start playback of the selected track", "Play") + * tooltip: i18n("Start playback of the selected track") + * }, + * Kirigami.Action { + * enabled: false + * iconName: "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 { + * iconName: "documentinfo" + * text: i18nc("Show track metadata", "View details") + * tooltip: i18n("Show track metadata") + * }, + * Kirigami.Action { + * iconName: "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 { + * iconName: "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 { + + /** + * @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 Item contentHeader + + /** + * @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 + + Controls.Control { + id: columnHeader + topPadding: 0 + bottomPadding: 0 + leftPadding: 0 + rightPadding: 0 + contentItem: contentHeader + } + + Repeater { + model: actions + + delegate: Kirigami.BasicListItem { + Layout.fillWidth: true + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + + iconSize: Kirigami.Units.gridUnit + leftPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing + rightPadding: Kirigami.Units.largeSpacing + + Kirigami.Units.smallSpacing + + icon: modelData.icon.name + text: modelData.text + onClicked: modelData.trigger(this) + + enabled: modelData.enabled + + visible: modelData.visible + + Controls.ToolTip.visible: modelData.tooltip !== "" && hoverHandler.hovered + Controls.ToolTip.text: modelData.tooltip + HoverHandler { id: hoverHandler } + } + } + } +} diff --git a/src/controls/NavigationTabBar.qml b/src/controls/NavigationTabBar.qml new file mode 100644 index 0000000..7f64f19 --- /dev/null +++ b/src/controls/NavigationTabBar.qml @@ -0,0 +1,284 @@ +/* + * Copyright 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQml 2.15 +import QtQuick.Templates 2.15 as T +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.19 as Kirigami +import QtGraphicalEffects 1.12 + +/** + * @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 org.kde.kirigami 2.19 as Kirigami + * + * import QtQuick 2.15 + * import QtQuick.Controls 2.15 + * import QtQuick.Layouts 1.15 + * import org.kde.kirigami 2.19 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 { + * iconName: "globe" + * text: "World" + * checked: worldPage.visible + * onTriggered: { + * if (!worldPage.visible) { + * while (pageStack.depth > 0) { + * pageStack.pop(); + * } + * pageStack.push(worldPage); + * } + * } + * }, + * Kirigami.Action { + * iconName: "player-time" + * text: "Timers" + * checked: timersPage.visible + * onTriggered: { + * if (!timersPage.visible) { + * while (pageStack.depth > 0) { + * pageStack.pop(); + * } + * pageStack.push(timersPage); + * } + * } + * }, + * Kirigami.Action { + * iconName: "chronometer" + * text: "Stopwatch" + * checked: stopwatchPage.visible + * onTriggered: { + * if (!stopwatchPage.visible) { + * while (pageStack.depth > 0) { + * pageStack.pop(); + * } + * pageStack.push(stopwatchPage); + * } + * } + * }, + * Kirigami.Action { + * iconName: "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 + */ + +T.ToolBar { + id: root + +//BEGIN properties + /** + * @brief This property holds the list of actions displayed in the toolbar. + */ + property list actions + + /** + * @brief The property holds the maximum width of the toolbar actions, before margins are added. + */ + property real maximumContentWidth: { + let minDelegateWidth = Kirigami.Units.gridUnit * 5; + // always have at least the width of 5 items (so small amounts of actions look natural) + return Math.max(minDelegateWidth * actions.length, minDelegateWidth * 5); + } + + /** + * @brief This property holds the background color of the toolbar. + * + * default: ``Kirigami.Theme.highlightColor`` + */ + property color backgroundColor: Kirigami.Theme.backgroundColor + + /** + * @brief This property holds the foreground color of the toolbar (text, icon). + */ + property color foregroundColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.85) + + /** + * @brief This property holds the highlight foreground color (text, icon when action is checked). + */ + property color highlightForegroundColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.85) + + /** + * @brief This property holds the color of the highlight bar when an action is checked. + * + * default: ``Kirigami.Theme.highlightColor`` + */ + property color highlightBarColor: Kirigami.Theme.highlightColor + + /** + * @brief This property sets whether the toolbar should provide its own shadow. + * + * default: ``true`` + */ + property bool shadow: true + + /** + * @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 sets whether the icon colors should be masked with a single color. + * + * This only applies to buttons generated by the actions property. + * + * default: ``true`` + * + * @since 5.96 + */ + property bool recolorIcons: true +//END properties + + onCurrentIndexChanged: { + if (currentIndex === -1) { + if (tabGroup.checkState !== Qt.Unchecked) { + tabGroup.checkState = Qt.Unchecked; + } + return; + } + if (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(); + } + } + } + + // 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: Math.ceil(Math.min(root.availableWidth, root.maximumContentWidth)) + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding) + + Kirigami.Theme.colorSet: Kirigami.Theme.Window + + background: Rectangle { // color & shadow + implicitHeight: Kirigami.Units.gridUnit * 3 + Kirigami.Units.smallSpacing * 2 + color: root.backgroundColor + RectangularGlow { + anchors.fill: parent + z: -1 + visible: root.shadow + glowRadius: 5 + spread: 0.3 + color: Qt.rgba(0.0, 0.0, 0.0, 0.15) + } + } + + // Using Row because setting just width is more convenient than having to set Layout.minimumWidth and Layout.maximumWidth + contentItem: Row { + 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 + + onCheckedButtonChanged: { + if (!checkedButton) { + return + } + if (root.currentIndex !== checkedButton.tabIndex) { + root.currentIndex = checkedButton.tabIndex; + } + } + } + + // Using an Instantiator instead of a Repeater allows us to use parent.visibleChildren.length without including a Repeater in that count. + Instantiator { + id: instantiator + model: root.actions + delegate: NavigationTabButton { + id: delegate + parent: root.contentItem + action: modelData + visible: modelData.visible + recolorIcon: root.recolorIcons + T.ButtonGroup.group: tabGroup + // Workaround setting the action when checkable is not explicitly set making tabs uncheckable + onActionChanged: action.checkable = true + + foregroundColor: root.foregroundColor + highlightForegroundColor: root.highlightForegroundColor + highlightBarColor: root.highlightBarColor + } + } +} + + diff --git a/src/controls/NavigationTabButton.qml b/src/controls/NavigationTabButton.qml new file mode 100644 index 0000000..685cd32 --- /dev/null +++ b/src/controls/NavigationTabButton.qml @@ -0,0 +1,215 @@ +/* SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Templates 2.15 as T +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.19 as Kirigami + +/** + * @brief Navigation buttons to be used for the NavigationTabBar component. + * + * 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 (let i = 0; i < parent.children.length; ++i) { + if (parent.children[i] === this) return tabIdx + // Checking for AbstractButtons because any AbstractButton can act as a tab + if (parent.children[i] instanceof T.AbstractButton) { + ++tabIdx + } + } + return -1 + } + + /** + * @brief This property sets whether the icon colors should be masked with a single color. + * + * default: ``true`` + * + * @since 5.96 + */ + property bool recolorIcon: true + + property color foregroundColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.85) + property color highlightForegroundColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.85) + property color highlightBarColor: Kirigami.Theme.highlightColor + + property color pressedColor: Qt.rgba(highlightBarColor.r, highlightBarColor.g, highlightBarColor.b, 0.3) + property color hoverSelectColor: Qt.rgba(highlightBarColor.r, highlightBarColor.g, highlightBarColor.b, 0.2) + property color checkedBorderColor: Qt.rgba(highlightBarColor.r, highlightBarColor.g, highlightBarColor.b, 0.7) + property color pressedBorderColor: Qt.rgba(highlightBarColor.r, highlightBarColor.g, highlightBarColor.b, 0.9) + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + width: { + // Counting buttons because Repeaters can be counted among visibleChildren + let visibleButtonCount = 0, minWidth = height * 0.75; + for (let i = 0; i < parent.visibleChildren.length; ++i) { + if (parent.width / visibleButtonCount >= minWidth && // make buttons go off the screen if there is physically no room for them + parent.visibleChildren[i] instanceof T.AbstractButton) { // Checking for AbstractButtons because any AbstractButton can act as a tab + ++visibleButtonCount + } + } + + return Math.round(parent.width / visibleButtonCount) + } + + Kirigami.Theme.colorSet: Kirigami.Theme.Window + Kirigami.Theme.inherit: false + + // not using the hover handler built into control, since it seems to misbehave and + // permanently report hovered after a touch event + HoverHandler { + id: hoverHandler + } + + padding: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing + + icon.height: Kirigami.Units.iconSizes.smallMedium + icon.width: Kirigami.Units.iconSizes.smallMedium + icon.color: control.checked ? control.highlightForegroundColor : control.foregroundColor + + background: Rectangle { + Kirigami.Theme.colorSet: Kirigami.Theme.Button + Kirigami.Theme.inherit: false + + implicitHeight: 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.smallSpacing + color: control.pressed ? pressedColor : (control.checked || hoverHandler.hovered ? hoverSelectColor : "transparent") + + border.color: control.checked ? checkedBorderColor : (control.pressed ? pressedBorderColor : color) + border.width: 1 + + Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } } + Behavior on border.color { ColorAnimation { duration: Kirigami.Units.shortDuration } } + } + } + + contentItem: ColumnLayout { + spacing: label.lineCount > 1 ? 0 : control.spacing + + Kirigami.Icon { + id: icon + source: control.icon.name || control.icon.source + isMask: control.recolorIcon + Layout.alignment: Qt.AlignHCenter | (label.lineCount > 1 ? 0 : Qt.AlignBottom) + implicitHeight: source ? control.icon.height : 0 + implicitWidth: source ? control.icon.width : 0 + visible: control.icon.name !== '' && control.icon.source !== '' + color: control.icon.color + Behavior on color { ColorAnimation {} } + Behavior on opacity { NumberAnimation {} } + } + QQC2.Label { + id: label + Kirigami.MnemonicData.enabled: control.enabled && control.visible + Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.MenuItem + Kirigami.MnemonicData.label: control.text + + text: Kirigami.MnemonicData.richTextLabel + Layout.alignment: icon.visible ? Qt.AlignHCenter | Qt.AlignTop : Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter + + wrapMode: Text.Wrap + elide: Text.ElideMiddle + color: control.checked ? control.highlightForegroundColor : control.foregroundColor + font.bold: control.checked + font.family: Kirigami.Theme.smallFont.family + font.pointSize: icon.visible ? Kirigami.Theme.smallFont.pointSize : Kirigami.Theme.defaultFont.pointSize * 1.20 // 1.20 is equivalent to level 2 heading + + Behavior on color { ColorAnimation {} } + Behavior on opacity { NumberAnimation {} } + + // Work around bold text changing implicit size + Layout.preferredWidth: boldMetrics.implicitWidth + Layout.preferredHeight: boldMetrics.implicitHeight * label.lineCount + Layout.fillWidth: true + + QQC2.Label { + id: boldMetrics + visible: false + text: parent.text + font.bold: true + font.family: Kirigami.Theme.smallFont.family + font.pointSize: Kirigami.Theme.smallFont.pointSize + horizontalAlignment: Text.AlignHCenter + wrapMode: QQC2.Label.Wrap + elide: Text.ElideMiddle + } + } + } +} diff --git a/src/controls/OverlayDrawer.qml b/src/controls/OverlayDrawer.qml new file mode 100644 index 0000000..c487d0a --- /dev/null +++ b/src/controls/OverlayDrawer.qml @@ -0,0 +1,149 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Templates 2.0 as T2 +import org.kde.kirigami 2.15 +import "private" +import "templates" as T + +/** + * 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 + */ +T.OverlayDrawer { + id: root + +//BEGIN Properties + focus: false + modal: true + drawerOpen: !modal + closePolicy: modal ? T2.Popup.CloseOnEscape | T2.Popup.CloseOnReleaseOutside : T2.Popup.NoAutoClose + handleVisible: interactive && (modal || !drawerOpen) && (typeof(applicationWindow)===typeof(Function) && applicationWindow() ? applicationWindow().controlsVisible : true) + interactive: Settings.hasTransientTouchInput || Settings.isMobile + + onPositionChanged: { + if (!modal && !root.peeking && !root.animating) { + position = 1; + } + } + + background: Rectangle { + color: Theme.backgroundColor + + Item { + parent: root.handle + anchors.fill: parent + + ShadowedRectangle { + id: handleGraphics + anchors.centerIn: parent + + Theme.colorSet: parent.parent.handleAnchor && parent.parent.handleAnchor.visible ? parent.parent.handleAnchor.Theme.colorSet : Theme.Button + + Theme.backgroundColor: parent.parent.handleAnchor && parent.parent.handleAnchor.visible ? parent.parent.handleAnchor.Theme.backgroundColor : undefined + + Theme.textColor: parent.parent.handleAnchor && parent.parent.handleAnchor.visible ? parent.parent.handleAnchor.Theme.textColor : undefined + + Theme.inherit: false + color: root.handle.pressed ? Theme.highlightColor : 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: Units.gridUnit / 2 + + width: Units.iconSizes.smallMedium + Units.smallSpacing * 2 + height: width + radius: 2 + Behavior on color { + ColorAnimation { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + Loader { + anchors.centerIn: handleGraphics + width: height + height: Units.iconSizes.smallMedium + + Theme.colorSet: handleGraphics.Theme.colorSet + Theme.backgroundColor: handleGraphics.Theme.backgroundColor + Theme.textColor: handleGraphics.Theme.textColor + + asynchronous: true + + source: { + var 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.hasOwnProperty("actions")) { + return Qt.resolvedUrl("templates/private/ContextIcon.qml"); + }else { + return ""; + } + } + onItemChanged: { + if(item) { + item.drawer = Qt.binding(function(){return root}); + item.color = Qt.binding(function(){return root.handle.pressed ? Theme.highlightedTextColor : Theme.textColor}); + } + } + } + } + + + Separator { + LayoutMirroring.enabled: false + // LayoutMirroring.childrenInherit: true + 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) + } + visible: !root.modal + } + 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: Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + } +} diff --git a/src/controls/OverlaySheet.qml b/src/controls/OverlaySheet.qml new file mode 100644 index 0000000..fb03394 --- /dev/null +++ b/src/controls/OverlaySheet.qml @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2016 by Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import org.kde.kirigami 2.12 as Kirigami +import "private" +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 + + leftInset: 0 + topInset: -Kirigami.Units.smallSpacing + rightInset: 0 + bottomInset: -Kirigami.Units.smallSpacing + + background: DefaultCardBackground {} +} diff --git a/src/controls/Page.qml b/src/controls/Page.qml new file mode 100644 index 0000000..b3d1fb8 --- /dev/null +++ b/src/controls/Page.qml @@ -0,0 +1,501 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.10 as Kirigami +import "private" +import QtQuick.Templates 2.1 as T2 +import QtQuick.Controls 2.1 as QQC2 + +/** + * 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 + /** + * @brief The default content padding is 1 gridUnit. + * // TODO: check if this is not displayed in the generated api doc. + */ + padding: Kirigami.Units.gridUnit + + /** + * @brief The bottom content padding. + * + * default: `this is bound to the height of the floating action buttons when present; if not, then verticalPadding.` + * // TODO: check if this is not displayed in the generated api doc. + */ + bottomPadding: actionButtons.item ? actionButtons.height : verticalPadding + + /** + * @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 Defines the contextual actions for the page: + * an easy way to assign actions in the right sliding panel + * + * Example usage: + * @code + * import org.kde.kirigami 2.4 as Kirigami + * + * Kirigami.ApplicationWindow { + * [...] + * contextDrawer: Kirigami.ContextDrawer { + * id: contextDrawer + * } + * [...] + * } + * @endcode + * + * @code + * import org.kde.kirigami 2.4 as Kirigami + * + * Kirigami.Page { + * [...] + * actions.contextualActions: [ + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * }, + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * } + * ] + * [...] + * } + * @endcode + * + * @warning This will likely be removed someday. + * @property list contextualActions + */ + // TODO: remove + property alias contextualActions: actionsGroup.contextualActions + + /** + * @brief An optional single action for the action button. + * + * Example usage: + * @code + * import org.kde.kirigami 2.4 as Kirigami + * Kirigami.Page { + * actions.main: Kirigami.Action { + * icon.name: "edit" + * onTriggered: { + * // do stuff + * } + * } + * } + * @endcode + * @warning This will likely be removed someday. + * @property Action mainAction + */ + //TODO: remove + property alias mainAction: actionsGroup.main + + /** + * @brief An optional extra action at the left of the main action button. + * + * Example usage: + * + * @code + * import org.kde.kirigami 2.4 as Kirigami + * Kirigami.Page { + * actions.left: Kirigami.Action { + * icon.name: "edit" + * onTriggered: { + * // do stuff + * } + * } + * } + * @endcode + * @warning This will likely be removed someday. + * @property Action leftAction + */ + // TODO: remove + property alias leftAction: actionsGroup.left + + /** + * @brief An optional extra action at the right of the main action button. + * + * Example usage: + * @code + * import org.kde.kirigami 2.4 as Kirigami + * Kirigami.Page { + * actions.right: Kirigami.Action { + * icon.name: "edit" + * onTriggered: { + * // do stuff + * } + * } + * } + * @endcode + * @warning This will likely be removed someday. + * @property Action rightAction + */ + // TODO: remove + property alias rightAction: actionsGroup.right + + /** + * @brief This property holds the actions group. + * @code + * import org.kde.kirigami 2.4 as Kirigami + * Kirigami.Page { + * actions { + * main: Kirigami.Action {...} + * left: Kirigami.Action {...} + * right: Kirigami.Action {...} + * contextualActions: [ + * Kirigami.Action {...}, + * Kirigami.Action {...} + * ] + * } + * } + * @endcode + * @property org::kde::kirigami::private::PageActionPropertyGroup actions + */ + readonly property alias actions: actionsGroup + + /** + * Emitted when a visualization for the actions is about to be shown, + * such as the toolbar menu or the contextDrawer. + * + * @since 2.7 + */ + signal contextualActionsAboutToShow + + /** + * @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 + 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 ActionIconGroup icon: ActionIconGroup {} + + /** + * @brief Whether this page needs user attention. + */ + property bool needsAttention + + /** + * @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 + Kirigami.Heading { + Layout.fillWidth: true + Layout.maximumWidth: implicitWidth + 1 // The +1 is to make sure we do not trigger eliding at max width + Layout.minimumWidth: 0 + maximumLineCount: 1 + elide: Text.ElideRight + text: root.title + textFormat: Text.PlainText + } + } + + /** + * 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.Settings.isMobile ? Kirigami.ApplicationHeaderStyle.Titles : 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); + + + // Look for sheets and cose them + // FIXME: port Sheets to Popup? + onBackRequested: { + for(var i in root.resources) { + var item = root.resources[i]; + if (item.hasOwnProperty("close") && item.hasOwnProperty("sheetOpen") && item.sheetOpen) { + item.close() + event.accepted = true; + return; + } + } + } + + // NOTE: contentItem will be created if not existing (and contentChildren of Page would become its children) This with anchors enforces the geometry we want, where globalToolBar is a super-header, on top of header + contentItem: Item { + anchors { + top: (root.header && root.header.visible) + ? root.header.bottom + : (globalToolBar.visible ? globalToolBar.bottom : parent.top) + topMargin: root.topPadding + root.spacing + bottom: (root.footer && root.footer.visible) ? root.footer.top : parent.bottom + bottomMargin: root.bottomPadding + root.spacing + } + } + + background: Rectangle { + color: Kirigami.Theme.backgroundColor + } + + implicitHeight: ((header && header.visible) ? header.implicitHeight : 0) + ((footer && footer.visible) ? footer.implicitHeight : 0) + contentItem.implicitHeight + topPadding + bottomPadding + implicitWidth: contentItem.implicitWidth + leftPadding + rightPadding + + // FIXME: on material the shadow would bleed over + clip: root.header !== null; + + onHeaderChanged: { + if (header) { + header.anchors.top = Qt.binding(() => globalToolBar.visible ? globalToolBar.bottom : root.top); + } + } + + Component.onCompleted: { + headerChanged(); + parentChanged(root.parent); + globalToolBar.syncSource(); + actionButtons.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.T2.StackView.view) { + globalToolBar.stack = root.T2.StackView.view; + globalToolBar.row = root.T2.StackView.view ? root.T2.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: [ + PageActionPropertyGroup { + id: actionsGroup + }, + + 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) + Loader { + id: globalToolBar + z: 9999 + height: item ? item.implicitHeight : 0 + anchors { + left: parent.left + right: parent.right + top: parent.top + } + property Kirigami.PageRow row + property T2.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.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 + Loader { + id: actionButtons + z: 9999 + parent: root + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + // if the page doesn't inherit, assume it has custom colors we want to use them here too + Kirigami.Theme.inherit: !root.Kirigami.Theme.inherit + Kirigami.Theme.colorSet: Kirigami.Theme.Button + + // It should be T2.Page, Qt 5.7 doesn't like it + property Item page: root + height: item ? item.implicitHeight : 0 + + asynchronous: true + + property bool pageComplete: false + + 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; + } + + // Note: Do not use root.globalToolBarStyle here as it is + // evaluated too late and will cause active to be true for a + // brief period, triggering the loading process. + if (globalToolBar.row && globalToolBar.row.globalToolBar.actualStyle === Kirigami.ApplicationHeaderStyle.ToolBar) { + return false; + } + + if (!root.actions.main && !root.actions.left && !root.actions.right && root.actions.contextualActions.length === 0) { + return false; + } + + // Legacy + if (typeof applicationWindow === "undefined") { + return true; + } + + if (applicationWindow().header && applicationWindow().header.toString().indexOf("ToolBarApplicationHeader") !== -1) { + return false; + } + + if (applicationWindow().footer && applicationWindow().footer.toString().indexOf("ToolBarApplicationHeader") !== -1) { + return false; + } + + return true; + } + + source: Qt.resolvedUrl("./private/ActionButton.qml") + } + ] + + Layout.fillWidth: true +} diff --git a/src/controls/PagePoolAction.qml b/src/controls/PagePoolAction.qml new file mode 100644 index 0000000..7fa15a8 --- /dev/null +++ b/src/controls/PagePoolAction.qml @@ -0,0 +1,227 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.7 +import QtQml 2.7 +import QtQuick.Controls 2.5 as Controls +import org.kde.kirigami 2.11 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 Controls.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() { + 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() { + if (!useLayers || !pageStack.hasOwnProperty("layers")) return false + + var found = pageStack.layers.find((item, index) => { + return item === pagePool.pageForUrl(page) + }) + return found ? true: false + } + + /** + * @returns true if the page has been loaded and placed on the pageStack, + * otherwise returns null. + */ + function stackContainsPage() { + 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. + 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() + } + } + + let pageStack_ = useLayers ? pageStack.layers : pageStack + + if (initialProperties && typeof(initialProperties) !== "object") { + console.warn("initialProperties must be of type object"); + return; + } + + if (!pageStack_.hasOwnProperty("pop") || typeof pageStack_.pop !== "function" || !pageStack_.hasOwnProperty("push") || typeof pageStack_.push !== "function") { + return; + } + + if (pagePool.isLocalUrl(page)) { + if (basePage) { + pageStack_.pop(basePage); + + } else if (!useLayers) { + pageStack_.clear(); + } + + pageStack_.push(initialProperties ? + pagePool.loadPageWithProperties(page, initialProperties) : + pagePool.loadPage(page)); + } else { + var callback = function(item) { + if (basePage) { + pageStack_.pop(basePage); + + } else if (!useLayers) { + pageStack_.clear(); + } + pageStack_.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() { + pageStack.layers.clear() + } + + property list connections: [ + Connections { + target: 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: pageStack.hasOwnProperty("layers") + target: pageStack.layers + + function onCurrentItemChanged() { + if (root.useLayers && root.checkable) { + _private.setChecked(root.layerContainsPage()); + + } else { + if (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..cc60e10 --- /dev/null +++ b/src/controls/PageRow.qml @@ -0,0 +1,980 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.2 +import QtQml.Models 2.2 +import QtQuick.Templates 2.0 as T +import QtQuick.Controls 2.0 as QQC2 +import org.kde.kirigami 2.7 +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. Items 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.Controls.Control + */ +T.Control { + id: root + +//BEGIN PROPERTIES + /** + * @brief This property holds the number of items currently pushed onto the view. + * @property int depth + */ + 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 + */ + 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 variant 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 + */ + 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 + */ + 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::firstVisibleItem + * @property Item firstVisibleItem + * @since 2.6 + */ + property alias firstVisibleItem: columnView.firstVisibleItem + + /** + * @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::lastVisibleItem + * @property Item lastVisibleItem + * @since 2.6 + */ + property alias lastVisibleItem: columnView.lastVisibleItem + + /** + * @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: 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: root.width >= root.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. + * * ``TabBar``: The global toolbar will look like a TabBar for choosing which page to display. + * * ``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 ApplicationHeaderStyle.ShowBackButton and 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. + * * ``maximumHeight: int `` The height the toolbar will have in mobile mode when the app is in reachable mode (default: preferredHeight * 1.5). + * * ``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 +//END PROPERTIES + +//BEGIN FUNCTIONS + /** + * @brief Pushes a page on the stack. + * + * The page can be defined as a component, item or string. + * If an item is used then the page will get re-parented. + * If a string is used then it is interpreted as a url that is used to load a page + * component. + * The last pushed page will become the current item. + * + * @param page The page can also be given as an array of pages. + * In this case all those pages will + * be pushed onto the stack. The items in the stack can be components, items or + * strings just like for single pages. + * Additionally an object can be used, which specifies a page and an optional + * properties property. + * This can be used to push multiple pages while still giving each of + * them properties. + * When an array is used the transition animation will only be to the last page. + * + * @param properties The properties argument is optional and allows defining a + * map of properties to set on the page. If page is actually an array of pages, properties should also be an array of key/value maps + * @return The new created page (or the last one if it was an array) + */ + function push(page, properties) { + var item = insertPage(depth, 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 The page can be defined as a component, item or string. If an item is + * used then the page will get re-parented. If a string is used then it + * is interpreted as a url that is used to load a page component. Once + * pushed the page gains the methods `closeDialog` allowing to close itself. + * Kirigami only supports calling `closeDialog` once. + * @param properties The properties given when initializing the page. + * @param windowProperties The properties given to the initialized window on desktop. + * @return The new created page + */ + function pushDialogLayer(page, properties = {}, windowProperties = {}) { + let item; + if (Settings.isMobile) { + if (QQC2.ApplicationWindow.window.width > Units.gridUnit * 40) { + // open as a QQC2.Dialog + const dialog = Qt.createQmlObject(' + import QtQuick 2.15; + import QtQuick.Controls 2.15; + import QtQuick.Layouts 1.15; + import org.kde.kirigami 2.15 as Kirigami; + Dialog { + id: dialog + modal: true; + leftPadding: 0; rightPadding: 0; topPadding: 0; bottomPadding: 0; + clip: true + header: Kirigami.AbstractApplicationHeader { + pageRow: null + page: null + minimumHeight: Units.gridUnit * 1.6 + maximumHeight: Units.gridUnit * 1.6 + preferredHeight: Units.gridUnit * 1.6 + + Keys.onEscapePressed: { + 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: dialog.close(); + } + } + } + } + contentItem: Control { topPadding: 0; leftPadding: 0; rightPadding: 0; bottomPadding: 0; } + }', QQC2.ApplicationWindow.overlay); + dialog.width = Qt.binding(() => QQC2.ApplicationWindow.window.width - Units.gridUnit * 5); + dialog.height = Qt.binding(() => QQC2.ApplicationWindow.window.height - Units.gridUnit * 5); + dialog.x = Units.gridUnit * 2.5; + dialog.y = 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); + 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; + } + dialog.title = Qt.binding(() => item.title); + + // Pushing a PageRow is supported but without PageRow toolbar + if (item.globalToolBar && item.globalToolBar.style) { + item.globalToolBar.style = ApplicationHeaderStyle.None + } + Object.defineProperty(item, 'closeDialog', { + value: function() { + dialog.close(); + } + }); + dialog.open(); + } else { + // open as a layer + item = layers.push(page, properties); + Object.defineProperty(item, 'closeDialog', { + value: function() { + layers.pop(); + } + }); + } + } else { + // open as a new window + if (!windowProperties.modality) { + windowProperties.modality = Qt.WindowModal; + } + if (!windowProperties.height) { + windowProperties.height = Units.gridUnit * 30; + } + if (!windowProperties.width) { + windowProperties.width = Units.gridUnit * 50; + } + if (!windowProperties.minimumWidth) { + windowProperties.minimumWidth = Units.gridUnit * 20; + } + if (!windowProperties.minimumHeight) { + windowProperties.minimumHeight = Units.gridUnit * 15; + } + if (!windowProperties.flags) { + windowProperties.flags = Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint; + } + const windowComponent = Qt.createComponent(Qt.resolvedUrl("./ApplicationWindow.qml")); + const window = windowComponent.createObject(root, windowProperties); + item = window.pageStack.push(page, properties); + Object.defineProperty(item, 'closeDialog', { + value: function() { + window.close(); + } + }); + } + item.Keys.escapePressed.connect(function() { item.closeDialog() }); + return item; + } + + /** + * @brief Inserts a new page or a list of new pages at an arbitrary position. + * + * The page can be defined as a component, item or string. + * If an item is used then the page will get re-parented. + * If a string is used then it is interpreted as a url that is used to load a page + * component. + * The current Page will not be changed, currentIndex will be adjusted + * accordingly if needed to keep the same current page. + * + * @param page The page can also be given as an array of pages. + * In this case all those pages will + * be pushed onto the stack. The items in the stack can be components, items or + * strings just like for single pages. + * Additionally an object can be used, which specifies a page and an optional + * properties property. + * This can be used to push multiple pages while still giving each of + * them properties. + * When an array is used the transition animation will only be to the last page. + * + * @param properties The properties argument is optional and allows defining a + * map of properties to set on the page. If page is actually an array of pages, properties should also be an array of key/value maps + * @return The new created page (or the last one if it was an array) + * @since 2.7 + */ + function insertPage(position, page, properties) { + if (!page) { + return null + } + //don't push again things already there + if (page.createObject === undefined && typeof page !== "string" && columnView.containsItem(page)) { + print("The item " + page + " is already in the PageRow"); + return null; + } + + position = Math.max(0, Math.min(depth, position)); + + columnView.pop(columnView.currentItem); + + // figure out if more than one page is being pushed + var pages; + var propsArray = []; + if (page instanceof Array) { + pages = page; + page = pages.pop(); + //compatibility with pre-qqc1 api, can probably be removed + if (page.createObject === undefined && page.parent === undefined && typeof page !== "string") { + properties = properties || page.properties; + page = page.page; + } + } + if (properties instanceof Array) { + propsArray = properties; + properties = propsArray.pop(); + } else { + propsArray = [properties]; + } + + // push any extra defined pages onto the stack + if (pages) { + var i; + for (i = 0; i < pages.length; i++) { + var tPage = pages[i]; + var tProps = propsArray[i]; + //compatibility with pre-qqc1 api, can probably be removed + if (tPage.createObject === undefined && tPage.parent === undefined && typeof tPage !== "string") { + if (columnView.containsItem(tPage)) { + print("The item " + page + " is already in the PageRow"); + continue; + } + tProps = tPage.properties; + tPage = tPage.page; + } + + var pageItem = pagesLogic.initAndInsertPage(position, tPage, tProps); + ++position; + } + } + + // initialize the page + var pageItem = pagesLogic.initAndInsertPage(position, page, properties); + + pagePushed(pageItem); + + return pageItem; + } + + /** + * 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) { + 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) { + if (depth === 0) { + return null; + } + + return columnView.removeItem(page); + } + + /** + * @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) { + if (depth === 0) { + return null; + } + + return columnView.pop(page); + } + + /** + * @brief Replaces a page on the stack. + * @param page The page can also be given as an array of pages. + * In this case all those pages will + * be pushed onto the stack. The items in the stack can be components, items or + * strings just like for single pages. + * the current page and all pagest after it in the stack will be removed. + * Additionally an object can be used, which specifies a page and an optional + * properties property. + * This can be used to push multiple pages while still giving each of + * them properties. + * When an array is used the transition animation will only be to the last page. + * @param properties The properties argument is optional and allows defining a + * map of properties to set on the page. + * @see push() for details. + */ + function replace(page, properties) { + if (!page) { + return null; + } + + // Remove all pages on top of the one being replaced. + if (currentIndex >= 0) { + columnView.pop(columnView.contentChildren[currentIndex]); + } else { + console.warn("There's no page to replace"); + } + + // Figure out if more than one page is being pushed. + var pages; + var 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. + var 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) { + var i; + for (i = 0; i < pages.length; i++) { + var tPage = pages[i]; + var tProps = propsArray[i]; + + var 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() { + return columnView.clear(); + } + + /** + * @return the page at idx + * @param idx the depth of the page we want + */ + function get(idx) { + return columnView.contentChildren[idx]; + } + + /** + * Go back to the previous index and scroll to the left to show one more column. + */ + function flickBack() { + 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) { + 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 (root.currentIndex >= 1) { + try { // app code might be screwy, but we still want to continue functioning if it throws an exception + root.currentItem.backRequested(backEvent) + } catch (error) {} + + if (!backEvent.accepted) { + if (root.depth > 1) { + root.currentIndex = Math.max(0, root.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() { + root.currentIndex = Math.min(root.depth-1, root.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: { + 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: root.leftSidebar + function onModalChanged() { + if (leftSidebar.modal) { + let sidebar = sidebarControl.contentItem; + let background = sidebarControl.background; + sidebarControl.contentItem = null; + leftSidebar.contentItem = sidebar; + sidebarControl.background = null; + leftSidebar.background = background; + + sidebar.visible = true; + background.visible = true; + } else { + let sidebar = leftSidebar.contentItem + let 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: root.goBack() + } + Shortcut { + sequences: [ StandardKey.Forward ] + onActivated: root.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: layersStack + z: 99 + anchors { + fill: parent + } + // placeholder as initial item + initialItem: columnViewLayout + + function clear () { + // don't let it kill the main page row + var d = layersStack.depth; + for (var i = 1; i < d; ++i) { + pop(); + } + } + + popEnter: Transition { + OpacityAnimator { + from: 0 + to: 1 + duration: Units.longDuration + easing.type: Easing.InOutCubic + } + } + popExit: Transition { + ParallelAnimation { + OpacityAnimator { + from: 1 + to: 0 + duration: Units.longDuration + easing.type: Easing.InOutCubic + } + YAnimator { + from: 0 + to: height/2 + duration: 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: Units.longDuration + easing.type: Easing.InOutCubic + } + YAnimator { + from: height/2 + to: 0 + duration: Units.longDuration + easing.type: Easing.OutCubic + } + } + } + + + pushExit: Transition { + OpacityAnimator { + from: 1 + to: 0 + duration: Units.longDuration + easing.type: Easing.InOutCubic + } + } + + replaceEnter: Transition { + ParallelAnimation { + OpacityAnimator { + from: 0 + to: 1 + duration: Units.longDuration + easing.type: Easing.InOutCubic + } + YAnimator { + from: height/2 + to: 0 + duration: Units.longDuration + easing.type: Easing.OutCubic + } + } + } + + replaceExit: Transition { + ParallelAnimation { + OpacityAnimator { + from: 1 + to: 0 + duration: Units.longDuration + easing.type: Easing.InCubic + } + YAnimator { + from: 0 + to: -height/2 + duration: Units.longDuration + easing.type: Easing.InOutCubic + } + } + } + } + + Loader { + id: globalToolBarUI + anchors { + left: parent.left + top: parent.top + right: parent.right + } + z: 100 + property T.Control pageRow: root + active: (!firstVisibleItem || firstVisibleItem.globalToolBarStyle !== ApplicationHeaderStyle.None) && + (globalToolBar.actualStyle !== ApplicationHeaderStyle.None || (firstVisibleItem && firstVisibleItem.globalToolBarStyle === 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() + + function getPageComponent(page) { + var 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) { + var pageComp = getPageComponent(page, properties); + + if (pageComp) { + // instantiate page from component + // FIXME: parent directly to columnView or root? + page = pageComp.createObject(null, properties || {}); + + if (pageComp.status === Component.Error) { + throw new Error("Error while loading page: " + pageComp.errorString()); + } + } else { + // copy properties to the page + for (var prop in properties) { + if (properties.hasOwnProperty(prop)) { + page[prop] = properties[prop]; + } + } + } + return page; + } + + function initAndInsertPage(position, page, properties) { + page = initPage(page, properties); + columnView.insertItem(position, page); + return page; + } + } + + RowLayout { + id: columnViewLayout + spacing: 1 + readonly property alias columnView: columnView + QQC2.Control { + id: sidebarControl + Layout.fillHeight: true + visible: contentItem !== null && root.leftDrawer && root.leftDrawer.visible + 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 + } + 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: Settings.isMobile + columnResizeMode: root.wideMode ? ColumnView.FixedColumns : ColumnView.SingleColumn + columnWidth: root.defaultColumnWidth + + onItemInserted: root.pageInserted(position, item); + onItemRemoved: root.pageRemoved(item); + } + } + + Rectangle { + anchors.bottom: parent.bottom + height: Units.smallSpacing + x: (columnView.width - width) * (columnView.contentX / (columnView.contentWidth - columnView.width)) + width: columnView.width * (columnView.width/columnView.contentWidth) + color: Theme.textColor + opacity: 0 + onXChanged: { + opacity = 0.3 + scrollIndicatorTimer.restart(); + } + Behavior on opacity { + OpacityAnimator { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + Timer { + id: scrollIndicatorTimer + interval: 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..25b37f2 --- /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 2.15 +import org.kde.kirigami 2.20 as Kirigami + +/** + * @brief This is a standard password text field. + * + * Example usage: + * @code{.qml} + * import org.kde.kirigami 2.20 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 { + icon.name: root.showPassword ? "password-show-off" : "password-show-on" + onTriggered: root.showPassword = !root.showPassword + } + + Keys.onPressed: { + 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..2338258 --- /dev/null +++ b/src/controls/PlaceholderMessage.qml @@ -0,0 +1,272 @@ +/* + * SPDX-FileCopyrightText: 2020 by Nate Graham + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.0 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 as QQC2 + +import org.kde.kirigami 2.12 as Kirigami + +import "private" + +/** + * @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 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 2.12 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 2.12 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 2.12 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" + * } + * } + * @endcode + * @code{.qml} + * import org.kde.kirigami 2.12 as Kirigami + * + ** Used as a loading indicator + * Kirigami.Page { + * id: root + * readonly property bool loading: [...] + * readonly property int completionStatus: [...] + * + * Kirigami.PlaceholderMessage { + * anchors.centerIn: parent + * width: parent.width - (Kirigami.Units.largeSpacing * 4) + * + * visible: root.loading + * + * icon.name: "my-awesome-app-icon" + * text: "Loading this awesome app" + * + * ProgressBar { + * Layout.preferredWidth: Kirigami.Units.gridUnit * 20 + * value: root.completionStatus + * from: 0 + * to: 100 + * } + * } + * } + * @endcode + * @code{.qml} + * import org.kde.kirigami 2.12 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 && 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 + */ + property ActionIconGroup icon: ActionIconGroup {} + + /** + * @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 +//END properties + + spacing: Kirigami.Units.largeSpacing + + Kirigami.Icon { + visible: source !== undefined + opacity: 0.5 + + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.round(Kirigami.Units.iconSizes.huge * 1.5) + Layout.preferredHeight: Math.round(Kirigami.Units.iconSizes.huge * 1.5) + + 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 + + wrapMode: Text.WordWrap + } + + QQC2.Label { + text: root.explanation + visible: root.explanation !== "" + opacity: root.type === PlaceholderMessage.Type.Actionable ? 1 : 0.65 + + horizontalAlignment: Qt.AlignHCenter + wrapMode: Text.WordWrap + + Layout.fillWidth: true + } + + QQC2.Button { + id: actionButton + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Kirigami.Units.gridUnit + + visible: action && action.enabled + } +} diff --git a/src/controls/PromptDialog.qml b/src/controls/PromptDialog.qml new file mode 100644 index 0000000..22e86ad --- /dev/null +++ b/src/controls/PromptDialog.qml @@ -0,0 +1,121 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 as Controls +import org.kde.kirigami 2.20 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: "New Folder" + * + * standardButtons: Kirigami.Dialog.None + * customFooterActions: [ + * Kirigami.Action { + * text: qsTr("Create Folder") + * iconName: "dialog-ok" + * onTriggered: { + * showPassiveNotification("Created"); + * textPromptDialog.close(); + * } + * }, + * Kirigami.Action { + * text: qsTr("Cancel") + * iconName: "dialog-cancel" + * onTriggered: { + * textPromptDialog.close(); + * } + * } + * ] + * + * Controls.TextField { + * placeholderText: qsTr("Folder name...") + * } + * } + * @endcode + * + * @inherit Dialog + */ +Kirigami.Dialog { + default property alias mainItem: control.contentItem + + /** + * 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: contentPadding + + /** + * 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 + + padding: 0 // we want content padding, not padding of the scrollview + preferredWidth: Kirigami.Units.gridUnit * 18 + + Controls.Control { + id: control + topPadding: contentTopPadding + bottomPadding: contentBottomPadding + leftPadding: contentLeftPadding + rightPadding: contentRightPadding + + contentItem: Kirigami.SelectableLabel { + text: subtitle + wrapMode: Controls.Label.Wrap + } + } +} diff --git a/src/controls/RouterWindow.qml b/src/controls/RouterWindow.qml new file mode 100644 index 0000000..8e62d8e --- /dev/null +++ b/src/controls/RouterWindow.qml @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import org.kde.kirigami 2.12 as Kirigami + +/** + * @brief An ApplicationWindow with a preconfigured PageRouter. + * + * In order to call functions on the PageRouter, use @link PageRouterAttached the attached Kirigami.PageRouter object @endlink. + * + * @inherit org::kde::kirigami::ApplicationWindow + */ +Kirigami.ApplicationWindow { + id: __kirigamiApplicationWindow + + /** + * @see org::kde::kirigami::PageRouter::routes + * @property list route + */ + default property alias routes: __kirigamiPageRouter.routes + + /** + * @see org::kde::kirigami::PageRouter::initialRoute + * @property string initialRoute + */ + property alias initialRoute: __kirigamiPageRouter.initialRoute + + /** + * @brief This property holds this window's PageRouter. + * @property org::kde::kirigami::PageRouter + */ + property alias router: __kirigamiPageRouter + + Kirigami.PageRouter { + id: __kirigamiPageRouter + pageStack: __kirigamiApplicationWindow.pageStack.columnView + } +} diff --git a/src/controls/ScrollablePage.qml b/src/controls/ScrollablePage.qml new file mode 100644 index 0000000..b54fa5e --- /dev/null +++ b/src/controls/ScrollablePage.qml @@ -0,0 +1,358 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Templates 2.15 as T +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 +import QtGraphicalEffects 1.0 + +import org.kde.kirigami 2.19 +import org.kde.kirigami.templates 2.2 as KT +import "private" + +/** + * @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: BasicListItem {} + * } + * } + * [...] + * @endcode + */ +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 This property sets the horizontal scrollbar policy. + * @property Qt::ScrollBarPolicy horizontalScrollBarPolicy + */ + property int horizontalScrollBarPolicy: QQC2.ScrollBar.AlwaysOff + + 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 + + contentHeight: flickable ? flickable.contentHeight : 0 + implicitHeight: { + let height = contentHeight + topPadding + bottomPadding; + if (header && header.visible) { + height += header.implicitHeight; + } + if (footer && footer.visible) { + height += footer.implicitHeight; + } + return height; + } + + implicitWidth: { + let width = 0; + if (flickable) { + if (flickable.contentItem) { + return flickable.contentItem.implicitWidth; + } else { + return contentItem.implicitWidth + leftPadding + rightPadding; + } + } else { + return 0; + } + } + + Theme.inherit: false + Theme.colorSet: flickable && flickable.hasOwnProperty("model") ? Theme.View : 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 && root.header.visible) + ? root.header.bottom + // FIXME: for now assuming globalToolBarItem is in a Loader, which needs to be get rid of + : (globalToolBarItem && globalToolBarItem.parent && globalToolBarItem.visible + ? globalToolBarItem.parent.bottom + : parent.top) + bottom: (root.footer && root.footer.visible) ? root.footer.top : parent.bottom + left: parent.left + right: parent.right + topMargin: root.refreshing ? busyIndicatorLoader.height : 0 + Behavior on topMargin { + NumberAnimation { + easing.type: Easing.InOutQuad + duration: Units.longDuration + } + } + } + QQC2.ScrollBar.horizontal.policy: root.horizontalScrollBarPolicy + QQC2.ScrollBar.vertical.policy: root.verticalScrollBarPolicy + } + + data: [ + // Has to be a MouseArea that accepts events otherwise touch events on Wayland will get lost + MouseArea { + id: scrollingArea + width: root.flickable.width + height: Math.max(root.flickable.height, implicitHeight) + implicitHeight: { + let impl = 0; + for (let i in itemsParent.visibleChildren) { + let child = itemsParent.visibleChildren[i]; + if (child.implicitHeight <= 0) { + impl = Math.max(impl, child.height); + } else { + impl = Math.max(impl, child.implicitHeight); + } + } + return impl + itemsParent.anchors.topMargin + itemsParent.anchors.bottomMargin; + } + Item { + id: itemsParent + property Flickable flickable + anchors { + fill: parent + leftMargin: root.leftPadding + topMargin: root.topPadding + rightMargin: root.rightPadding + bottomMargin: root.bottomPadding + } + onChildrenChanged: { + let 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 + } + }, + + Loader { + id: busyIndicatorLoader + z: 99 + y: root.flickable.verticalLayoutDirection === ListView.BottomToTop + ? -root.flickable.contentY + root.flickable.originY + height + : -root.flickable.contentY + root.flickable.originY - height + width: root.flickable.width + height: Units.gridUnit * 4 + active: root.supportsRefreshing + + sourceComponent: Item { + id: busyIndicatorFrame + + QQC2.BusyIndicator { + id: busyIndicator + z: 1 + anchors.centerIn: parent + running: root.refreshing + visible: root.refreshing + // Android busywidget QQC seems to be broken at custom sizes + } + Rectangle { + id: spinnerProgress + anchors { + fill: busyIndicator + margins: Math.ceil(Units.smallSpacing) + } + radius: width + visible: supportsRefreshing && !refreshing && progress > 0 + color: "transparent" + opacity: 0.8 + border.color: Theme.backgroundColor + border.width: Math.ceil(Units.smallSpacing) + property real progress: supportsRefreshing && !refreshing ? (busyIndicatorLoader.y/busyIndicatorFrame.height) : 0 + } + ConicalGradient { + source: spinnerProgress + visible: spinnerProgress.visible + anchors.fill: spinnerProgress + gradient: Gradient { + GradientStop { position: 0.00; color: Theme.highlightColor } + GradientStop { position: spinnerProgress.progress; color: Theme.highlightColor } + GradientStop { position: spinnerProgress.progress + 0.01; color: "transparent" } + GradientStop { position: 1.00; color: "transparent" } + } + } + + Connections { + target: busyIndicatorLoader + function onYChanged() { + if (!supportsRefreshing) { + return; + } + + if (!root.refreshing && busyIndicatorLoader.y > busyIndicatorFrame.height/2 + topPadding) { + refreshTriggerTimer.running = true; + } else { + refreshTriggerTimer.running = false; + } + } + } + Timer { + id: refreshTriggerTimer + interval: 500 + onTriggered: { + if (!root.refreshing && busyIndicatorLoader.y > busyIndicatorFrame.height/2 + topPadding) { + root.refreshing = true; + } + } + } + } + } + ] + + Component.onCompleted: { + let flickableFound = false; + for (let i in itemsParent.data) { + let child = itemsParent.data[i]; + 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.left = undefined; + root.flickable.anchors.right = undefined; + root.flickable.anchors.top = undefined; + root.flickable.anchors.bottom = undefined; + } else { + scrollView.contentItem = root.flickable; + scrollingArea.parent = root.flickable.contentItem; + root.flickable.contentHeight = Qt.binding(() => { return scrollingArea.implicitHeight - root.flickable.topMargin - root.flickable.bottomMargin }); + root.flickable.contentWidth = Qt.binding(() => { return scrollingArea.implicitWidth }); + } + root.flickable.flickableDirection = Flickable.VerticalFlick; + } +} diff --git a/src/controls/SearchField.qml b/src/controls/SearchField.qml new file mode 100644 index 0000000..e178f89 --- /dev/null +++ b/src/controls/SearchField.qml @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: 2019 Carl-Lucien Schwan + * SPDX-FileCopyrightText: 2022 Felipe Kinoshita + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.6 +import QtQuick.Controls 2.1 as Controls +import org.kde.kirigami 2.16 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 2.8 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 (Qt.application.layoutDirection === Qt.RightToLeft) { + return _rightActionsRow.width + Kirigami.Units.smallSpacing + } else { + return (activeFocus || root.text.length > 0 ? 0 : (searchIcon.width + Kirigami.Units.smallSpacing)) + Kirigami.Units.smallSpacing * 2 + } + rightPadding: if (Qt.application.layoutDirection === Qt.RightToLeft) { + return (activeFocus || root.text.length > 0 ? 0 : (searchIcon.width + Kirigami.Units.smallSpacing)) + Kirigami.Units.smallSpacing * 2 + } else { + return _rightActionsRow.width + Kirigami.Units.smallSpacing + } + + Kirigami.Icon { + id: searchIcon + opacity: parent.activeFocus || text.length > 0 ? 0 : 1 + anchors.left: parent.left + anchors.leftMargin: Kirigami.Units.smallSpacing * 2 + anchors.bottom: parent.bottom + anchors.bottomMargin: Math.round((parent.implicitHeight - implicitHeight) / 2 + (parent.bottomPadding - parent.topPadding) / 2) + implicitHeight: Kirigami.Units.iconSizes.sizeForLabels + implicitWidth: Kirigami.Units.iconSizes.sizeForLabels + color: root.placeholderTextColor + + source: "search" + + Behavior on opacity { + NumberAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + + placeholderText: qsTr("Search…") + + Accessible.name: qsTr("Search") + Accessible.searchEdit: true + + focusSequence: "Ctrl+F" + inputMethodHints: Qt.ImhNoPredictiveText + rightActions: [ + Kirigami.Action { + icon.name: root.LayoutMirroring.enabled ? "edit-clear-locationbar-ltr" : "edit-clear-locationbar-rtl" + visible: root.text.length > 0 + onTriggered: { + root.text = ""; + // 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: { + 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..05fc2e1 --- /dev/null +++ b/src/controls/SelectableLabel.qml @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2022 Fushan Wen + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 + +/** + * @brief This is a label which supports text selection. + * + * You can use all elements of the QML TextArea component, in particular + * the "text" property to define the label text. + * + * Example usage: + * @code{.qml} + * Kirigami.SelectableLabel { + * text: "Label" + * } + * @endcode + * + * @see https://bugreports.qt.io/browse/QTBUG-14077 + * @since 5.95 + * @since org.kde.kirigami 2.20 + * @inherit QtQuick.Controls.TextArea + */ +QQC2.TextArea { + id: selectableLabel + + /** + * @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 + + padding: 0 + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + readOnly: true + wrapMode: Text.WordWrap + textFormat: TextEdit.AutoText + verticalAlignment: TextEdit.AlignTop + + Accessible.selectableText: true + Accessible.editable: false + + background: Item {} + + HoverHandler { + id: hoverHandler + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor + } +} diff --git a/src/controls/Separator.qml b/src/controls/Separator.qml new file mode 100644 index 0000000..02624a1 --- /dev/null +++ b/src/controls/Separator.qml @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2012 Marco Martin + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.12 + +/** + * @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 + + 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: ``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: ColorUtils.linearInterpolation(Theme.backgroundColor, Theme.textColor, weight === Separator.Weight.Light ? 0.07 : 0.15) +} diff --git a/src/controls/ShadowedImage.qml b/src/controls/ShadowedImage.qml new file mode 100644 index 0000000..030bec9 --- /dev/null +++ b/src/controls/ShadowedImage.qml @@ -0,0 +1,133 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * SPDX-FileCopyrightText: 2022 Carl Schwan + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import org.kde.kirigami 2.12 + +/** + * @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 2.19 + * + * 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 { + +//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 the scaled width and height of the full-frame image. + * @see QtQuick.Image::sourceSize + */ + property alias sourceSize: image.sourceSize +//END properties + + Image { + id: image + anchors.fill: parent + visible: shadowRectangle.softwareRendering + } + + ShadowedTexture { + id: shadowRectangle + anchors.fill: parent + + source: image.status === Image.Ready ? image : null + } +} diff --git a/src/controls/SwipeListItem.qml b/src/controls/SwipeListItem.qml new file mode 100644 index 0000000..7d31052 --- /dev/null +++ b/src/controls/SwipeListItem.qml @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2010 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import "private" +import "templates" as T + +/** + * 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 org::kde::kirigami::templates::SwipeListItem + */ +T.SwipeListItem { + id: listItem + + background: DefaultListItemBackground {} +} diff --git a/src/controls/ToolBarApplicationHeader.qml b/src/controls/ToolBarApplicationHeader.qml new file mode 100644 index 0000000..9367815 --- /dev/null +++ b/src/controls/ToolBarApplicationHeader.qml @@ -0,0 +1,122 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import QtQuick.Controls 2.0 as Controls +import QtQuick.Layouts 1.2 +import "private" +import org.kde.kirigami 2.4 + + +/** + * @brief ToolBarApplicationHeader represents a toolbar that + * will display the actions of the current page. + * + * Both Contextual actions and the main, left and right actions + */ +ApplicationHeader { + id: header + + preferredHeight: 42 + maximumHeight: preferredHeight + headerStyle: ApplicationHeaderStyle.Titles + + // FIXME: needs a property definition to have its own type in qml + property string _internal: "" + + Component.onCompleted: print("Warning: ToolbarApplicationHeader is deprecated, remove and use the automatic internal toolbar instead.") + pageDelegate: Item { + id: delegateItem + readonly property bool current: __appWindow.pageStack.currentIndex === index + implicitWidth: titleTextMetrics.width/2 + buttonTextMetrics.collapsedButtonsWidth + + RowLayout { + id: titleLayout + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + right: actionsLayout.left + } + Separator { + id: separator + Layout.preferredHeight: parent.height * 0.6 + } + + Heading { + id: title + Layout.fillWidth: true + + Layout.preferredWidth: implicitWidth + Layout.minimumWidth: Math.min(titleTextMetrics.width, delegateItem.width - buttonTextMetrics.requiredWidth) + leftPadding: Units.largeSpacing + opacity: delegateItem.current ? 1 : 0.4 + maximumLineCount: 1 + color: Theme.textColor + elide: Text.ElideRight + text: page ? page.title : "" + } + } + + TextMetrics { + id: titleTextMetrics + text: page ? page.title : "" + font: title.font + } + TextMetrics { + id: buttonTextMetrics + text: (page.actions.left ? page.actions.left.text : "") + (page.actions.main ? page.actions.main.text : "") + (page.actions.right ? page.actions.right.text : "") + readonly property int collapsedButtonsWidth: ctxActionsButton.width + (page.actions.left ? ctxActionsButton.width + Units.gridUnit : 0) + (page.actions.main ? ctxActionsButton.width + Units.gridUnit : 0) + (page.actions.right ? ctxActionsButton.width + Units.gridUnit : 0) + readonly property int requiredWidth: width + collapsedButtonsWidth + } + + RowLayout { + id: actionsLayout + anchors { + verticalCenter: parent.verticalCenter + right: ctxActionsButton.visible ? ctxActionsButton.left : parent.right + } + + readonly property bool toobig: delegateItem.width - titleTextMetrics.width - Units.gridUnit < buttonTextMetrics.requiredWidth + + PrivateActionToolButton { + Layout.alignment: Qt.AlignVCenter + action: page && page.actions ? page.actions.left : null + display: parent.toobig ? Controls.AbstractButton.IconOnly : Controls.AbstractButton.TextBesideIcon + } + PrivateActionToolButton { + Layout.alignment: Qt.AlignVCenter + Layout.rightMargin: Units.smallSpacing + action: page && page.actions ? page.actions.main : null + display: parent.toobig ? Controls.AbstractButton.IconOnly : Controls.AbstractButton.TextBesideIcon + flat: false + } + PrivateActionToolButton { + Layout.alignment: Qt.AlignVCenter + action: page && page.actions ? page.actions.right : null + display: parent.toobig ? Controls.AbstractButton.IconOnly : Controls.AbstractButton.TextBesideIcon + } + } + + PrivateActionToolButton { + id: ctxActionsButton + showMenuArrow: page.actions.contextualActions.length === 1 + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + rightMargin: Units.smallSpacing + } + Action { + id: overflowAction + icon.name: "overflow-menu" + tooltip: qsTr("More Actions") + visible: children.length > 0 + children: page && page.actions.contextualActions ? page.actions.contextualActions : null + } + + action: page && page.actions.contextualActions.length === 1 ? page.actions.contextualActions[0] : overflowAction + } + } +} diff --git a/src/controls/UrlButton.qml b/src/controls/UrlButton.qml new file mode 100644 index 0000000..307c055 --- /dev/null +++ b/src/controls/UrlButton.qml @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.2 +import org.kde.kirigami.private 2.6 as KirigamiPrivate +import QtQuick.Controls 2.1 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 + */ +LinkButton { + id: button + + property string url + + text: url + enabled: !!url + visible: text.length > 0 + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onPressed: if (mouse.button === Qt.RightButton) { + menu.popup() + } + onClicked: if (mouse.button !== Qt.RightButton) { + Qt.openUrlExternally(url) + } + + QQC2.ToolTip { + // If button's text has been overridden, show a tooltip to expose the raw URL + visible: button.text !== button.url && button.mouseArea.containsMouse + text: 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/ActionButton.qml b/src/controls/private/ActionButton.qml new file mode 100644 index 0000000..df645a3 --- /dev/null +++ b/src/controls/private/ActionButton.qml @@ -0,0 +1,560 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.0 as Controls +import QtGraphicalEffects 1.0 +import org.kde.kirigami 2.16 + +import "../templates/private" + +Item { + id: root + + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + bottomMargin: root.page.footer ? root.page.footer.height : 0 + } + //smallSpacing for the shadow + implicitHeight: button.height + Units.smallSpacing + clip: true + + readonly property Page page: root.parent.page + //either Action or QAction should work here + + function isActionAvailable(action) { return action && (action.hasOwnProperty("visible") ? action.visible === undefined || action.visible : !action.hasOwnProperty("visible")); } + + readonly property QtObject action: root.page && isActionAvailable(root.page.mainAction) ? root.page.mainAction : null + readonly property QtObject leftAction: root.page && isActionAvailable(root.page.leftAction) ? root.page.leftAction : null + readonly property QtObject rightAction: root.page && isActionAvailable(root.page.rightAction) ? root.page.rightAction : null + + readonly property bool hasApplicationWindow: typeof applicationWindow !== "undefined" && applicationWindow + readonly property bool hasGlobalDrawer: typeof globalDrawer !== "undefined" && globalDrawer + readonly property bool hasContextDrawer: typeof contextDrawer !== "undefined" && contextDrawer + + transform: Translate { + id: translateTransform + } + + states: [ + State { + when: mouseArea.internalVisibility + PropertyChanges { + target: translateTransform + y: 0 + } + PropertyChanges { + target: root + opacity: 1 + } + PropertyChanges { + target: root + visible: true + } + }, + State { + when: !mouseArea.internalVisibility + PropertyChanges { + target: translateTransform + y: button.height + } + PropertyChanges { + target: root + opacity: 0 + } + PropertyChanges { + target: root + visible: false + } + } + ] + transitions: Transition { + ParallelAnimation { + NumberAnimation { + target: translateTransform + property: "y" + duration: Units.longDuration + easing.type: mouseArea.internalVisibility ? Easing.InQuad : Easing.OutQuad + } + OpacityAnimator { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + + onWidthChanged: button.x = Qt.binding(() => (root.width / 2 - button.width / 2)) + Item { + id: button + x: root.width/2 - button.width/2 + + // We have a mismatch in releases in removing the mobile x1.5 sizing (kirigami is part of frameworks, but styles are part of plasma releases) + // Remove after Plasma 5.23 is released, and switch back to Units.iconSizes.medium/large + property int mediumIconSizing: Units.iconSizes.sizeForLabels * 2 + property int largeIconSizing: Units.iconSizes.sizeForLabels * 3 + + anchors.bottom: edgeMouseArea.bottom + + implicitWidth: implicitHeight + mediumIconSizing*2 + Units.gridUnit + implicitHeight: largeIconSizing + Units.largeSpacing*2 + + + onXChanged: { + if (mouseArea.pressed || edgeMouseArea.pressed || fakeContextMenuButton.pressed) { + if (root.hasGlobalDrawer && globalDrawer.enabled && globalDrawer.modal) { + globalDrawer.peeking = true; + globalDrawer.visible = true; + if (Qt.application.layoutDirection === Qt.LeftToRight) { + globalDrawer.position = Math.min(1, Math.max(0, (x - root.width/2 + button.width/2)/globalDrawer.contentItem.width + mouseArea.drawerShowAdjust)); + } else { + globalDrawer.position = Math.min(1, Math.max(0, (root.width/2 - button.width/2 - x)/globalDrawer.contentItem.width + mouseArea.drawerShowAdjust)); + } + } + if (root.hasContextDrawer && contextDrawer.enabled && contextDrawer.modal) { + contextDrawer.peeking = true; + contextDrawer.visible = true; + if (Qt.application.layoutDirection === Qt.LeftToRight) { + contextDrawer.position = Math.min(1, Math.max(0, (root.width/2 - button.width/2 - x)/contextDrawer.contentItem.width + mouseArea.drawerShowAdjust)); + } else { + contextDrawer.position = Math.min(1, Math.max(0, (x - root.width/2 + button.width/2)/contextDrawer.contentItem.width + mouseArea.drawerShowAdjust)); + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + + visible: action !== null || leftAction !== null || rightAction !== null + property bool internalVisibility: (!root.hasApplicationWindow || (applicationWindow().controlsVisible && applicationWindow().height > root.height*2)) && (root.action === null || root.action.visible === undefined || root.action.visible) + preventStealing: true + + drag { + target: button + //filterChildren: true + axis: Drag.XAxis + minimumX: root.hasContextDrawer && contextDrawer.enabled && contextDrawer.modal ? 0 : root.width/2 - button.width/2 + maximumX: root.hasGlobalDrawer && globalDrawer.enabled && globalDrawer.modal ? root.width : root.width/2 - button.width/2 + } + + property var downTimestamp; + property int startX + property int startMouseY + property real drawerShowAdjust + + readonly property int currentThird: (3*mouseX)/width + readonly property QtObject actionUnderMouse: { + switch(currentThird) { + case 0: return leftAction; + case 1: return action; + case 2: return rightAction; + default: return null + } + } + + hoverEnabled: true + + Controls.ToolTip.visible: containsMouse && !Settings.tabletMode && actionUnderMouse + Controls.ToolTip.text: actionUnderMouse ? actionUnderMouse.text : "" + Controls.ToolTip.delay: Units.toolTipDelay + + onPressed: { + // search if we have a page to set to current + if (root.hasApplicationWindow && applicationWindow().pageStack.currentIndex !== undefined && root.page.ColumnView.level !== undefined) { + // search the button parent's parent, that is the page parent + // this will make the context drawer open for the proper page + applicationWindow().pageStack.currentIndex = root.page.ColumnView.level; + } + downTimestamp = (new Date()).getTime(); + startX = button.x + button.width/2; + startMouseY = mouse.y; + drawerShowAdjust = 0; + } + onReleased: { + tooltipHider.restart(); + if (root.hasGlobalDrawer) globalDrawer.peeking = false; + if (root.hasContextDrawer) contextDrawer.peeking = false; + // pixel/second + var x = button.x + button.width/2; + var speed = ((x - startX) / ((new Date()).getTime() - downTimestamp) * 1000); + drawerShowAdjust = 0; + + // project where it would be a full second in the future + if (root.hasContextDrawer && root.hasGlobalDrawer && globalDrawer.modal && x + speed > Math.min(root.width/4*3, root.width/2 + globalDrawer.contentItem.width/2)) { + globalDrawer.open(); + contextDrawer.close(); + } else if (root.hasContextDrawer && x + speed < Math.max(root.width/4, root.width/2 - contextDrawer.contentItem.width/2)) { + if (root.hasContextDrawer && contextDrawer.modal) { + contextDrawer.open(); + } + if (root.hasGlobalDrawer && globalDrawer.modal) { + globalDrawer.close(); + } + } else { + if (root.hasGlobalDrawer && globalDrawer.modal) { + globalDrawer.close(); + } + if (root.hasContextDrawer && contextDrawer.modal) { + contextDrawer.close(); + } + } + // Don't rely on native onClicked, but fake it here: + // Qt.startDragDistance is not adapted to devices dpi in case + // of Android, so consider the button "clicked" when: + // *the button has been dragged less than a gridunit + // *the finger is still on the button + if (Math.abs((button.x + button.width/2) - startX) < Units.gridUnit && + mouse.y > 0) { + + //if an action has been assigned, trigger it + if (actionUnderMouse && actionUnderMouse.trigger) { + actionUnderMouse.trigger(); + } + + if (actionUnderMouse && actionUnderMouse.hasOwnProperty("children") && actionUnderMouse.children.length > 0) { + var subMenuUnderMouse; + switch (actionUnderMouse) { + case leftAction: + subMenuUnderMouse = leftActionSubMenu; + break; + case mainAction: + subMenuUnderMouse = mainActionSubMenu; + break + case rightAction: + subMenuUnderMouse = rightActionSubMenu; + break; + } + if (subMenuUnderMouse && !subMenuUnderMouse.visible) { + subMenuUnderMouse.visible = true; + } + } + } + } + + onPositionChanged: { + drawerShowAdjust = Math.min(0.3, Math.max(0, (startMouseY - mouse.y)/(Units.gridUnit*15))); + button.xChanged(); + } + onPressAndHold: { + if (!actionUnderMouse) { + return; + } + + // if an action has been assigned, show a message like a tooltip + if (actionUnderMouse && actionUnderMouse.text && Settings.tabletMode) { + tooltipHider.stop(); + Controls.ToolTip.show(actionUnderMouse.text); + // The tooltip is shown perpetually while we are pressed and held, and + // we start tooltipHider below when the press is released. This ensures + // that the user can have as much time as they want to read the tooltip, + // and also that the tooltip is hidden in a pleasant manner that does + // not feel overly urgent. + } + } + Timer { + id: tooltipHider + interval: Units.humanMoment + onTriggered: { + Controls.ToolTip.hide(); + } + } + Connections { + target: root.hasGlobalDrawer ? globalDrawer : null + function onPositionChanged() { + if ( globalDrawer && globalDrawer.modal && !mouseArea.pressed && !edgeMouseArea.pressed && !fakeContextMenuButton.pressed) { + if (Qt.application.layoutDirection === Qt.LeftToRight) { + button.x = globalDrawer.contentItem.width * globalDrawer.position + root.width/2 - button.width/2; + } else { + button.x = -globalDrawer.contentItem.width * globalDrawer.position + root.width/2 - button.width/2 + } + } + } + } + Connections { + target: root.hasContextDrawer ? contextDrawer : null + function onPositionChanged() { + if (contextDrawer && contextDrawer.modal && !mouseArea.pressed && !edgeMouseArea.pressed && !fakeContextMenuButton.pressed) { + if (Qt.application.layoutDirection === Qt.LeftToRight) { + button.x = root.width/2 - button.width/2 - contextDrawer.contentItem.width * contextDrawer.position; + } else { + button.x = root.width/2 - button.width/2 + contextDrawer.contentItem.width * contextDrawer.position; + } + } + } + } + + Item { + id: background + anchors { + fill: parent + } + + Rectangle { + id: buttonGraphics + radius: width/2 + anchors.centerIn: parent + height: parent.height - Units.smallSpacing*2 + width: height + enabled: root.action && root.action.enabled + visible: root.action + readonly property bool pressed: root.action && root.action.enabled && ((root.action === mouseArea.actionUnderMouse && mouseArea.pressed) || root.action.checked) + property color baseColor: root.action && root.action.icon && root.action.icon.color && root.action.icon.color !== undefined && root.action.icon.color.a > 0 ? root.action.icon.color : Theme.highlightColor + color: pressed ? Qt.darker(baseColor, 1.3) : baseColor + + ActionsMenu { + id: mainActionSubMenu + y: -height + x: -width/2 + parent.width/2 + actions: root.action && root.action.hasOwnProperty("children") ? root.action.children : "" + submenuComponent: Component { + ActionsMenu {} + } + } + Icon { + id: icon + anchors.centerIn: parent + width: button.mediumIconSizing + height: width + source: root.action && root.action.icon.name ? root.action.icon.name : "" + selected: true + color: root.action && root.action.icon && root.action.icon.color && root.action.icon.color.a > 0 ? root.action.icon.color : (selected ? Theme.highlightedTextColor : Theme.textColor) + } + Behavior on color { + ColorAnimation { + duration: Units.shortDuration + easing.type: Easing.InOutQuad + } + } + Behavior on x { + NumberAnimation { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + // left button + Rectangle { + id: leftButtonGraphics + z: -1 + anchors { + left: parent.left + bottom: parent.bottom + bottomMargin: Units.smallSpacing + } + enabled: root.leftAction && root.leftAction.enabled + radius: 2 + height: button.mediumIconSizing + Units.smallSpacing * 2 + width: height + (root.action ? Units.gridUnit*2 : 0) + visible: root.leftAction + + readonly property bool pressed: root.leftAction && root.leftAction.enabled && ((mouseArea.actionUnderMouse === root.leftAction && mouseArea.pressed) || root.leftAction.checked) + property color baseColor: root.leftAction && root.leftAction.icon && root.leftAction.icon.color && root.leftAction.icon.color !== undefined && root.leftAction.icon.color.a > 0 ? root.leftAction.icon.color : Theme.highlightColor + color: pressed ? baseColor : Theme.backgroundColor + Behavior on color { + ColorAnimation { + duration: Units.shortDuration + easing.type: Easing.InOutQuad + } + } + ActionsMenu { + id: leftActionSubMenu + y: -height + x: -width/2 + parent.width/2 + actions: root.leftAction && root.leftAction.hasOwnProperty("children") ? root.leftAction.children : "" + submenuComponent: Component { + ActionsMenu {} + } + } + Icon { + source: root.leftAction && root.leftAction.icon.name ? root.leftAction.icon.name : "" + width: button.mediumIconSizing + height: width + selected: leftButtonGraphics.pressed + color: root.leftAction && root.leftAction.icon && root.leftAction.icon.color && root.leftAction.icon.color.a > 0 ? root.leftAction.icon.color : (selected ? Theme.highlightedTextColor : Theme.textColor) + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + margins: root.action ? Units.smallSpacing * 2 : Units.smallSpacing + } + } + } + //right button + Rectangle { + id: rightButtonGraphics + z: -1 + anchors { + right: parent.right + // verticalCenter: parent.verticalCenter + bottom: parent.bottom + bottomMargin: Units.smallSpacing + } + enabled: root.rightAction && root.rightAction.enabled + radius: 2 + height: button.mediumIconSizing + Units.smallSpacing * 2 + width: height + (root.action ? Units.gridUnit*2 : 0) + visible: root.rightAction + readonly property bool pressed: root.rightAction && root.rightAction.enabled && ((mouseArea.actionUnderMouse === root.rightAction && mouseArea.pressed) || root.rightAction.checked) + property color baseColor: root.rightAction && root.rightAction.icon && root.rightAction.icon.color && root.rightAction.icon.color !== undefined && root.rightAction.icon.color.a > 0 ? root.rightAction.icon.color : Theme.highlightColor + color: pressed ? baseColor : Theme.backgroundColor + Behavior on color { + ColorAnimation { + duration: Units.shortDuration + easing.type: Easing.InOutQuad + } + } + ActionsMenu { + id: rightActionSubMenu + y: -height + x: -width/2 + parent.width/2 + actions: root.rightAction && root.rightAction.hasOwnProperty("children") ? root.rightAction.children : "" + submenuComponent: Component { + ActionsMenu {} + } + } + Icon { + source: root.rightAction && root.rightAction.icon.name ? root.rightAction.icon.name : "" + width: button.mediumIconSizing + height: width + selected: rightButtonGraphics.pressed + color: root.rightAction && root.rightAction.icon && root.rightAction.icon.color && root.rightAction.icon.color.a > 0 ? root.rightAction.icon.color : (selected ? Theme.highlightedTextColor : Theme.textColor) + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + margins: root.action ? Units.smallSpacing * 2 : Units.smallSpacing + } + } + } + } + + DropShadow { + anchors.fill: background + horizontalOffset: 0 + verticalOffset: 1 + radius: Units.gridUnit /2 + samples: 16 + color: Qt.rgba(0, 0, 0, mouseArea.pressed ? 0.6 : 0.4) + source: background + } + } + } + + MouseArea { + id: fakeContextMenuButton + anchors { + right: edgeMouseArea.right + bottom: parent.bottom + margins: Units.smallSpacing + } + drag { + target: button + axis: Drag.XAxis + minimumX: root.hasContextDrawer && contextDrawer.enabled && contextDrawer.modal ? 0 : root.width/2 - button.width/2 + maximumX: root.hasGlobalDrawer && globalDrawer.enabled && globalDrawer.modal ? root.width : root.width/2 - button.width/2 + } + visible: root.page.actions && root.page.actions.contextualActions.length > 0 && (applicationWindow === undefined || applicationWindow().wideScreen) + // using internal pagerow api + && (root.page && root.page.parent ? root.page.ColumnView.level < applicationWindow().pageStack.depth-1 : false) + + width: button.mediumIconSizing + Units.smallSpacing*2 + height: width + + + DropShadow { + anchors.fill: handleGraphics + horizontalOffset: 0 + verticalOffset: 1 + radius: Units.gridUnit /2 + samples: 16 + color: Qt.rgba(0, 0, 0, fakeContextMenuButton.pressed ? 0.6 : 0.4) + source: handleGraphics + } + Rectangle { + id: handleGraphics + anchors.fill: parent + color: fakeContextMenuButton.pressed ? Theme.highlightColor : Theme.backgroundColor + radius: 1 + Icon { + anchors.centerIn: parent + width: button.mediumIconSizing + selected: fakeContextMenuButton.pressed + height: width + source: "overflow-menu" + } + Behavior on color { + ColorAnimation { + duration: Units.shortDuration + easing.type: Easing.InOutQuad + } + } + } + + onPressed: { + mouseArea.onPressed(mouse) + } + onReleased: { + if (globalDrawer) { + globalDrawer.peeking = false; + } + if (contextDrawer) { + contextDrawer.peeking = false; + } + var pos = root.mapFromItem(fakeContextMenuButton, mouse.x, mouse.y); + if (contextDrawer) { + if (pos.x < root.width/2) { + contextDrawer.open(); + } else if (contextDrawer.drawerOpen && mouse.x > 0 && mouse.x < width) { + contextDrawer.close(); + } + } + if (globalDrawer) { + if (globalDrawer.position > 0.5) { + globalDrawer.open(); + } else { + globalDrawer.close(); + } + } + if (containsMouse && (!globalDrawer || !globalDrawer.drawerOpen || !globalDrawer.modal) && + (!contextDrawer || !contextDrawer.drawerOpen || !contextDrawer.modal)) { + contextMenu.visible = !contextMenu.visible; + } + } + ActionsMenu { + id: contextMenu + x: parent.width - width + y: -height + actions: root.page.actions.contextualActions + submenuComponent: Component { + ActionsMenu {} + } + } + } + + MouseArea { + id: edgeMouseArea + z:99 + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + drag { + target: button + //filterChildren: true + axis: Drag.XAxis + minimumX: root.hasContextDrawer && contextDrawer.enabled && contextDrawer.modal ? 0 : root.width/2 - button.width/2 + maximumX: root.hasGlobalDrawer && globalDrawer.enabled && globalDrawer.modal ? root.width : root.width/2 - button.width/2 + } + height: Units.smallSpacing * 3 + + onPressed: mouseArea.onPressed(mouse) + onPositionChanged: mouseArea.positionChanged(mouse) + onReleased: mouseArea.released(mouse) + } +} diff --git a/src/controls/private/ActionIconGroup.qml b/src/controls/private/ActionIconGroup.qml new file mode 100644 index 0000000..c64d75e --- /dev/null +++ b/src/controls/private/ActionIconGroup.qml @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQml 2.1 + +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..3654210 --- /dev/null +++ b/src/controls/private/ActionMenuItem.qml @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.3 +import QtQuick.Controls 2.3 as Controls +import org.kde.kirigami 2.4 as Kirigami + +Controls.MenuItem { + id: menuItem + + visible: action.visible === undefined || action.visible + height: visible ? implicitHeight : 0 + autoExclusive: action.Controls.ActionGroup && action.Controls.ActionGroup.group && action.Controls.ActionGroup.group.exclusive + + Controls.ToolTip.text: action.tooltip || "" + Controls.ToolTip.visible: menuItem.hovered && Controls.ToolTip.text.length>0 + Controls.ToolTip.delay: Kirigami.Units.toolTipDelay +} diff --git a/src/controls/private/ActionsMenu.qml b/src/controls/private/ActionsMenu.qml new file mode 100644 index 0000000..016c039 --- /dev/null +++ b/src/controls/private/ActionsMenu.qml @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as Controls + +import org.kde.kirigami 2.20 as Kirigami + +Controls.Menu +{ + id: theMenu + z: 999999999 + property alias actions: actionsInstantiator.model + property Component submenuComponent + // renamed to work on both Qt 5.9 and 5.10 + property Component itemDelegate: ActionMenuItem {} + property Component separatorDelegate: Controls.MenuSeparator { property var action } + property Component loaderDelegate: Loader { property var action } + property Controls.Action parentAction + property Controls.MenuItem parentItem + + Item { + id: invisibleItems + visible: false + } + Instantiator { + id: actionsInstantiator + + active: theMenu.visible + delegate: QtObject { + readonly property Controls.Action action: modelData + property QtObject item: null + + function create() { + if (!action.hasOwnProperty("children") && !action.children || action.children.length === 0) { + if (action.hasOwnProperty("separator") && action.separator) { + item = theMenu.separatorDelegate.createObject(null, { action: action }); + } + else if (action.displayComponent) { + item = theMenu.loaderDelegate.createObject(null, + { action: action, sourceComponent: action.displayComponent }); + } + else { + item = theMenu.itemDelegate.createObject(null, { action: action }); + } + theMenu.addItem(item) + } else if (theMenu.submenuComponent) { + item = theMenu.submenuComponent.createObject(null, { parentAction: action, title: action.text, actions: action.children }); + + theMenu.insertMenu(theMenu.count, item) + item.parentItem = theMenu.contentData[theMenu.contentData.length-1] + item.parentItem.icon = action.icon + } + } + function remove() { + if (!action.hasOwnProperty("children") && !action.children || action.children.length === 0) { + theMenu.removeItem(item) + } else if (theMenu.submenuComponent) { + theMenu.removeMenu(item) + } + } + } + + onObjectAdded: object.create() + onObjectRemoved: object.remove() + } +} diff --git a/src/controls/private/BannerImage.qml b/src/controls/private/BannerImage.qml new file mode 100644 index 0000000..6e1661c --- /dev/null +++ b/src/controls/private/BannerImage.qml @@ -0,0 +1,170 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.6 +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.12 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 + /* + * FIXME: compatibility + * + * @deprecated Please use `source` property instead! + */ + property alias imageSource: root.source + + /* + * FIXME: compatibility + * + * @deprecated Please use `titleIcon` property instead! + */ + property alias iconSource: root.titleIcon + + /** + * @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 + + 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 + + Layout.fillWidth: true + + Layout.preferredWidth: titleLayout.implicitWidth || sourceSize.width + Layout.preferredHeight: titleLayout.completed && source.toString() !== "" ? width/(sourceSize.width / sourceSize.height) : Layout.minimumHeight + Layout.minimumHeight: titleLayout.implicitHeight > 0 ? titleLayout.implicitHeight + Kirigami.Units.smallSpacing * 2 : 0 + + onTitleAlignmentChanged: { + titleLayout.implicitWidthChanged() + } + fillMode: Image.PreserveAspectCrop + asynchronous: true + + color: "transparent" + + Component.onCompleted: { + titleLayout.completed = true; + } + + Kirigami.ShadowedRectangle { + anchors { + left: parent.left + right: parent.right + top: (root.titleAlignment & Qt.AlignTop) ? parent.top : undefined + bottom: (root.titleAlignment & Qt.AlignBottom) ? parent.bottom : undefined + } + height: Math.min(parent.height, titleLayout.height * 1.5) + + opacity: 0.5 + color: "black" + + visible: root.source.toString() !== "" && root.title !== "" && ((root.titleAlignment & Qt.AlignTop) || (root.titleAlignment & Qt.AlignBottom)) + + 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 + } + + RowLayout { + id: titleLayout + property bool completed: false + anchors { + left: root.titleAlignment & Qt.AlignLeft ? parent.left : undefined + top: root.titleAlignment & Qt.AlignTop ? parent.top : undefined + right: root.titleAlignment & Qt.AlignRight ? parent.right : undefined + bottom: root.titleAlignment & Qt.AlignBottom ? parent.bottom : undefined + horizontalCenter: root.titleAlignment & Qt.AlignHCenter ? parent.horizontalCenter : undefined + verticalCenter: root.titleAlignment & Qt.AlignVCenter ? parent.verticalCenter : undefined + + leftMargin: root.leftPadding + topMargin: root.topPadding + rightMargin: root.rightPadding + bottomMargin: root.bottomPadding + } + width: Math.min(implicitWidth, parent.width -root.leftPadding - root.rightPadding) + 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() !== "" ? "white" : Kirigami.Theme.textColor + wrapMode: Text.NoWrap + elide: Text.ElideRight + } + } +} diff --git a/src/controls/private/CardsGridViewPrivate.qml b/src/controls/private/CardsGridViewPrivate.qml new file mode 100644 index 0000000..af3accb --- /dev/null +++ b/src/controls/private/CardsGridViewPrivate.qml @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.10 +import QtQuick.Controls 2.0 as Controls +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.4 as Kirigami + + +GridView { + id: root + + property Component _delegateComponent + + + QtObject { + id: calculations + + // initialize array so length property can be read + property var leftMargins: [] + readonly property int delegateWidth: Math.min(cellWidth, maximumColumnWidth) - Kirigami.Units.largeSpacing * 2 + } + + delegate: Kirigami.DelegateRecycler { + width: calculations.delegateWidth + + anchors.left: parent.left + + sourceComponent: root._delegateComponent + onWidthChanged: { + let columnIndex = index % root.columns + if (index < root.columns) { + // calulate left margin per column + calculations.leftMargins[columnIndex] = (width + Kirigami.Units.largeSpacing * 2) + * (columnIndex) + root.width / 2 + - (root.columns * (width + Kirigami.Units.largeSpacing * 2)) / 2; + } + anchors.leftMargin = calculations.leftMargins[columnIndex]; + } + } + onWidthChanged: { + if (calculations.leftMargins.length !== root.columns) { + calculations.leftMargins = new Array(root.columns); + } + } +} diff --git a/src/controls/private/ContextDrawerActionItem.qml b/src/controls/private/ContextDrawerActionItem.qml new file mode 100644 index 0000000..e21c301 --- /dev/null +++ b/src/controls/private/ContextDrawerActionItem.qml @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.6 +import QtQuick.Controls 2.0 as QQC2 +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.5 + +BasicListItem { + id: listItem + + readonly property bool isSeparator: modelData.hasOwnProperty("separator") && modelData.separator + + readonly property bool isExpandible: modelData && modelData.hasOwnProperty("expandible") && modelData.expandible + + checked: modelData.checked + icon: modelData.icon + separatorVisible: false + reserveSpaceForIcon: !isSeparator + reserveSpaceForLabel: !isSeparator + + label: model ? (model.text ? model.text : model.tooltip) : (modelData.text ? modelData.text : modelData.tooltip) + hoverEnabled: (!isExpandible || root.collapsed) && !Settings.tabletMode + sectionDelegate: isExpandible + font.pointSize: isExpandible ? Theme.defaultFont.pointSize * 1.30 : Theme.defaultFont.pointSize + + enabled: !isExpandible && !isSeparator && (model ? model.enabled : modelData.enabled) + visible: model ? model.visible : modelData.visible + opacity: enabled || isExpandible ? 1.0 : 0.6 + + Separator { + id: separatorAction + + visible: listItem.isSeparator + Layout.fillWidth: true + } + + ActionsMenu { + id: actionsMenu + y: Settings.isMobile ? -height : listItem.height + z: 99999999 + actions: modelData.children + submenuComponent: Component { + ActionsMenu {} + } + } + + Loader { + Layout.fillWidth: true + Layout.fillHeight: true + sourceComponent: modelData.displayComponent + onStatusChanged: { + for (let i in parent.children) { + const child = parent.children[i]; + if (child === this) { + child.visible = status === Loader.Ready; + break; + } else { + child.visible = status !== Loader.Ready; + } + } + } + Component.onCompleted: statusChanged() + } + + Icon { + isMask: true + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: Units.iconSizes.small/2 + selected: listItem.checked || listItem.pressed + Layout.preferredWidth: Layout.preferredHeight + source: "go-up-symbolic" + visible: !isExpandible && !listItem.isSeparator && modelData.children!== undefined && modelData.children.length > 0 + } + + onPressed: { + if (modelData.children.length > 0) { + actionsMenu.open(); + } + } + onClicked: { + if (modelData.children.length === 0) { + root.drawerOpen = false; + } + + if (modelData && modelData.trigger !== undefined) { + modelData.trigger(); + // assume the model is a list of QAction or Action + } else if (menu.model.length > index) { + menu.model[index].trigger(); + } else { + console.warning("Don't know how to trigger the action") + } + } +} diff --git a/src/controls/private/CornerShadow.qml b/src/controls/private/CornerShadow.qml new file mode 100644 index 0000000..5f37fc4 --- /dev/null +++ b/src/controls/private/CornerShadow.qml @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtGraphicalEffects 1.0 +import org.kde.kirigami 2.4 + +RadialGradient { + id: shadow + /** + * @brief This property holds the corner of the shadow that will determine + * the direction of the gradient. + * + * The acceptable values are: + * Qt.TopLeftCorner, TopRightCorner, BottomLeftCorner, BottomRightCorner + * + * default: ``Qt.TopRightCorner`` + * + * @see Qt::Corner + */ + property int corner: Qt.TopRightCorner + + readonly property real margin: -Math.floor(radius/3) + + property int radius: Units.gridUnit + + width: radius - margin + height: radius - margin + + horizontalRadius: width + verticalRadius: height + horizontalOffset: { + switch (corner) { + case Qt.TopLeftCorner: + case Qt.BottomLeftCorner: + return -width/2; + default: + return width/2; + } + } + verticalOffset: { + switch (corner) { + case Qt.TopLeftCorner: + case Qt.TopRightCorner: + return -width/2; + default: + return width/2; + } + } + + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.rgba(0, 0, 0, 0.25) + } + GradientStop { + position: 1 - radius/(radius - margin) + color: Qt.rgba(0, 0, 0, 0.25) + } + GradientStop { + position: 1 - radius/(radius - margin) + radius/(radius - margin) * 0.2 + color: Qt.rgba(0, 0, 0, 0.1) + } + GradientStop { + position: 1 - radius/(radius - margin) + radius/(radius - margin) * 0.35 + color: Qt.rgba(0, 0, 0, 0.02) + } + GradientStop { + position: 1.0 + color: "transparent" + } + } +} + diff --git a/src/controls/private/DefaultCardBackground.qml b/src/controls/private/DefaultCardBackground.qml new file mode 100644 index 0000000..67f43a2 --- /dev/null +++ b/src/controls/private/DefaultCardBackground.qml @@ -0,0 +1,90 @@ + +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ +import QtQuick 2.15 +import org.kde.kirigami 2.15 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.tintWithAlpha( + color, Kirigami.Theme.textColor, 0.2) +//END properties + + color: { + if (clickFeedback && (parent.down || parent.highlighted)) + return root.pressedColor + else if (hoverFeedback && parent.hovered) + return root.hoverColor + return defaultColor + } + radius: Kirigami.Units.smallSpacing + shadow { + size: Kirigami.Units.largeSpacing + color: Qt.rgba(0, 0, 0, 0.2) + yOffset: 2 + } + + border { + width: borderWidth + color: borderColor + } +} diff --git a/src/controls/private/DefaultChipBackground.qml b/src/controls/private/DefaultChipBackground.qml new file mode 100644 index 0000000..f842c05 --- /dev/null +++ b/src/controls/private/DefaultChipBackground.qml @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2022 Felipe Kinoshita +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.19 as Kirigami + +Rectangle { + /** + * @brief This property holds the color of the Chip's background when it is being pressed. + * @see QtQuick.AbstractButton::pressed + */ + 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 hoverSelectColor: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.2) + + /** + * @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.7) + + /** + * @brief This property holds the color of the Chip's border when it is being pressed. + * @see QtQuick.AbstractButton::pressed + */ + property color pressedBorderColor: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.9) + + Kirigami.Theme.colorSet:Kirigami.Theme.Header + Kirigami.Theme.inherit: false + + color: parent.pressed ? pressedColor : (parent.checked ? hoverSelectColor : Kirigami.Theme.backgroundColor) + border.color: parent.pressed ? checkedBorderColor : (parent.checked ? pressedBorderColor : Qt.darker(Kirigami.Theme.backgroundColor, 1.1)) + border.width: 1 + radius: 3 +} diff --git a/src/controls/private/DefaultListItemBackground.qml b/src/controls/private/DefaultListItemBackground.qml new file mode 100644 index 0000000..0a08ae4 --- /dev/null +++ b/src/controls/private/DefaultListItemBackground.qml @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import org.kde.kirigami 2.12 + +Rectangle { + id: background + color: listItem.checked || listItem.highlighted || (listItem.supportsMouseEvents && listItem.pressed && !listItem.checked && !listItem.sectionDelegate) + ? listItem.activeBackgroundColor + : (listItem.alternatingBackground && index%2 ? listItem.alternateBackgroundColor : listItem.backgroundColor) + + visible: listItem.ListView.view ? listItem.ListView.view.highlight === null : true + Rectangle { + id: internal + property bool indicateActiveFocus: listItem.pressed || Settings.tabletMode || listItem.activeFocus || (listItem.ListView.view ? listItem.ListView.view.activeFocus : false) + anchors.fill: parent + visible: !Settings.tabletMode && listItem.supportsMouseEvents + color: listItem.activeBackgroundColor + opacity: (listItem.hovered || listItem.highlighted || listItem.activeFocus) && !listItem.pressed ? 0.5 : 0 + } + // Don't show separator when... + readonly property bool __separatorVisible: listItem.separatorVisible + // There's a colored rectangle + && !listItem.highlighted + && !listItem.pressed + && !listItem.checked + // ...Unless the colored rectangle is transparent + && (!listItem.hovered || listItem.activeBackgroundColor.a === 0) + // It would touch the section header + && !listItem.sectionDelegate + && (!!listItem.ListView.view ? listItem.ListView.nextSection === listItem.ListView.section : true) + // This is the last item in the list + // TODO: implement this + + property var leadingWidth + + Separator { + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + leftMargin: Units.largeSpacing + rightMargin: Units.largeSpacing + } + visible: background.__separatorVisible + weight: Separator.Weight.Light + } +} + diff --git a/src/controls/private/EdgeShadow.qml b/src/controls/private/EdgeShadow.qml new file mode 100644 index 0000000..8ca79cd --- /dev/null +++ b/src/controls/private/EdgeShadow.qml @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtGraphicalEffects 1.0 +import org.kde.kirigami 2.4 + +LinearGradient { + 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: Units.gridUnit + implicitWidth: radius + implicitHeight: radius + + start: Qt.point((edge !== Qt.RightEdge ? 0 : width), (edge !== Qt.BottomEdge ? 0 : height)) + end: Qt.point((edge !== Qt.LeftEdge ? 0 : width), (edge !== Qt.TopEdge ? 0 : height)) + 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..d7947f0 --- /dev/null +++ b/src/controls/private/GlobalDrawerActionItem.qml @@ -0,0 +1,186 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.6 +import QtQuick.Window 2.6 +import QtQuick.Controls 2.0 as QQC2 +import QtQuick.Controls.impl 2.3 as QQC2Impl +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.5 + +AbstractListItem { + id: listItem + supportsMouseEvents: (!isExpandible || root.collapsed) + readonly property bool wideMode: width > height * 2 + readonly property bool isSeparator: modelData.hasOwnProperty("separator") && modelData.separator + + readonly property bool isExpandible: modelData && modelData.hasOwnProperty("expandible") && modelData.expandible + + checked: modelData.checked || (actionsMenu && actionsMenu.visible) + width: parent.width + + contentItem: RowLayout { + Icon { + id: iconItem + color: modelData.icon.color + source: modelData.icon.name || modelData.icon.source + + // We have a mismatch in releases in removing the mobile x1.5 sizing (kirigami is part of frameworks, but styles are part of plasma releases) + // Remove after Plasma 5.23 is released, and switch back to Units.iconSizes.medium/large + property int mediumIconSizing: Units.iconSizes.sizeForLabels * 2 + + property int size: Settings.isMobile ? mediumIconSizing : Units.iconSizes.smallMedium + Layout.minimumHeight: size + Layout.maximumHeight: size + Layout.minimumWidth: size + Layout.maximumWidth: size + selected: (listItem.highlighted || listItem.checked || (listItem.pressed && listItem.supportsMouseEvents)) + visible: source !== undefined + } + QQC2Impl.MnemonicLabel { + id: labelItem + text: width > height * 2 ? listItem.MnemonicData.mnemonicLabel : "" + Layout.fillWidth: true + mnemonicVisible: listItem.MnemonicData.active + color: (listItem.highlighted || listItem.checked || (listItem.pressed && listItem.supportsMouseEvents)) ? listItem.activeTextColor : listItem.textColor + elide: Text.ElideRight + font: listItem.font + // Work around Qt bug where NativeRendering breaks for non-integer scale factors + // https://bugreports.qt.io/browse/QTBUG-67007 + renderType: Screen.devicePixelRatio % 1 !== 0 ? Text.QtRendering : Text.NativeRendering + opacity: { + if (root.collapsed) { + return 0; + } else if (!listItem.enabled) { + return 0.6; + } else { + return 1.0; + } + } + Behavior on opacity { + NumberAnimation { + duration: Units.longDuration/2 + easing.type: Easing.InOutQuad + } + } + } + Separator { + id: separatorAction + + visible: listItem.isSeparator + Layout.fillWidth: true + } + + Icon { + Shortcut { + sequence: listItem.MnemonicData.sequence + onActivated: listItem.clicked() + } + isMask: true + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: !root.collapsed ? 0 : -width + Layout.preferredHeight: !root.collapsed ? Units.iconSizes.small : Units.iconSizes.small/2 + opacity: 0.7 + selected: listItem.checked || listItem.pressed + Layout.preferredWidth: Layout.preferredHeight + source: (LayoutMirroring.enabled ? "go-next-symbolic-rtl" : "go-next-symbolic") + visible: (!isExpandible || root.collapsed) && !listItem.isSeparator && modelData.hasOwnProperty("children") && modelData.children!==undefined && modelData.children.length > 0 + } + } + MnemonicData.enabled: listItem.enabled && listItem.visible + MnemonicData.controlType: MnemonicData.MenuItem + MnemonicData.label: modelData.text + property ActionsMenu actionsMenu: ActionsMenu { + x: Qt.application.layoutDirection === Qt.RightToLeft ? -width : listItem.width + actions: modelData.hasOwnProperty("children") ? modelData.children : null + submenuComponent: Component { + ActionsMenu {} + } + onVisibleChanged: { + if (visible) { + stackView.openSubMenu = listItem.actionsMenu; + } else if (stackView.openSubMenu === listItem.actionsMenu) { + stackView.openSubMenu = null; + } + } + } + + // TODO: animate the hide by collapse + visible: (model ? model.visible || model.visible===undefined : modelData.visible) && opacity > 0 + opacity: !root.collapsed || iconItem.source.length > 0 + Behavior on opacity { + NumberAnimation { + duration: Units.longDuration/2 + easing.type: Easing.InOutQuad + } + } + enabled: !isSeparator && ( (model && model.enabled !== undefined) ? model.enabled : modelData.enabled) + + hoverEnabled: (!isExpandible || root.collapsed) && !Settings.tabletMode + sectionDelegate: isExpandible + font.pointSize: isExpandible ? Theme.defaultFont.pointSize * 1.30 : Theme.defaultFont.pointSize + height: implicitHeight * opacity + + data: [ + QQC2.ToolTip { + visible: !listItem.isSeparator && (modelData.hasOwnProperty("tooltip") && modelData.tooltip.length || root.collapsed) && (!actionsMenu || !actionsMenu.visible) && listItem.hovered && text.length > 0 + text: modelData.hasOwnProperty("tooltip") && modelData.tooltip.length ? modelData.tooltip : modelData.text + delay: Units.toolTipDelay + timeout: 5000 + y: listItem.height/2 - height/2 + x: Qt.application.layoutDirection === Qt.RightToLeft ? -width : listItem.width + } + ] + + onHoveredChanged: { + if (!hovered) { + return; + } + if (stackView.openSubMenu) { + stackView.openSubMenu.visible = false; + + if (!listItem.actionsMenu.hasOwnProperty("count") || listItem.actionsMenu.count>0) { + if (listItem.actionsMenu.hasOwnProperty("popup")) { + listItem.actionsMenu.popup(listItem, listItem.width, 0) + } else { + listItem.actionsMenu.visible = true; + } + } + } + } + + onClicked: trigger() + Keys.onEnterPressed: trigger() + Keys.onReturnPressed: trigger() + + function trigger() { + if (!supportsMouseEvents) { + return; + } + modelData.trigger(); + if (modelData.hasOwnProperty("children") && modelData.children!==undefined && modelData.children.length > 0) { + if (root.collapsed) { + // fallbacks needed for Qt 5.9 + if ((!listItem.actionsMenu.hasOwnProperty("count") || listItem.actionsMenu.count>0) && !listItem.actionsMenu.visible) { + stackView.openSubMenu = listItem.actionsMenu; + if (listItem.actionsMenu.hasOwnProperty("popup")) { + listItem.actionsMenu.popup(listItem, listItem.width, 0) + } else { + listItem.actionsMenu.visible = true; + } + } + } else { + stackView.push(menuComponent, {model: modelData.children, level: level + 1, current: modelData }); + } + } else if (root.resetMenuOnTriggered) { + root.resetMenu(); + } + checked = Qt.binding(function() { return modelData.checked || (actionsMenu && actionsMenu.visible) }); + } + + Keys.onDownPressed: nextItemInFocusChain().focus = true + Keys.onUpPressed: nextItemInFocusChain(false).focus = true +} diff --git a/src/controls/private/PageActionPropertyGroup.qml b/src/controls/private/PageActionPropertyGroup.qml new file mode 100644 index 0000000..bf7567b --- /dev/null +++ b/src/controls/private/PageActionPropertyGroup.qml @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQml 2.1 + +QtObject { + property QtObject main + property QtObject left + property QtObject right + property list contextualActions +} + diff --git a/src/controls/private/PrivateActionToolButton.qml b/src/controls/private/PrivateActionToolButton.qml new file mode 100644 index 0000000..b088347 --- /dev/null +++ b/src/controls/private/PrivateActionToolButton.qml @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 as Controls + +import org.kde.kirigami 2.20 as Kirigami + +Controls.ToolButton { + id: control + + signal menuAboutToShow() + + Kirigami.Icon { + id: kirigamiIcon + visible: false + source: control.icon.name + } + + hoverEnabled: true + + display: Controls.ToolButton.TextBesideIcon + + property bool showMenuArrow: !Kirigami.DisplayHint.displayHintSet(action, Kirigami.DisplayHint.HideChildIndicator) + + property var menuActions: { + if (action && action.hasOwnProperty("children")) { + return Array.prototype.slice.call(action.children) + } + return [] + } + + property Component menuComponent: ActionsMenu { + submenuComponent: ActionsMenu { } + } + + property QtObject 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 = Controls.Popup.CloseOnEscape | Controls.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 && action.hasOwnProperty("visible")) ? action.visible : true + + // Workaround for QTBUG-85941 + Binding { + target: control + property: "checkable" + value: (control.action && control.action.checkable) || (control.menuActions && control.menuActions.length > 0) + } + + onToggled: { + if (menuActions.length > 0 && menu) { + if (checked) { + control.menuAboutToShow(); + menu.popup(control, 0, control.height) + } else { + menu.dismiss() + } + } + } + + Controls.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 === Controls.Button.IconOnly) { + return a.text; + } + } + return ""; + } + } + + // This is slightly ugly but saves us from needing to recreate the entire + // contents of the toolbutton. When using QQC2-desktop-style, the background + // will be an item that renders the entire control. We can simply set a + // property on it to get a menu arrow. + // TODO: Support other styles + Component.onCompleted: { + if (background.hasOwnProperty("showMenuArrow")) { + background.showMenuArrow = Qt.binding(() => control.showMenuArrow && control.menuActions.length > 0) + } + } +} diff --git a/src/controls/private/SwipeItemEventFilter.qml b/src/controls/private/SwipeItemEventFilter.qml new file mode 100644 index 0000000..3eeb0d7 --- /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 2.5 +import org.kde.kirigami 2.4 + + +MouseArea { + id: swipeFilter + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + } + + z: 99999 + property Item currentItem + property real peek + + preventStealing: true + width: Units.gridUnit + onPressed: { + var mapped = mapToItem(parent.flickableItem.contentItem, mouse.x, mouse.y); + currentItem = parent.flickableItem.itemAt(mapped.x, mapped.y); + } + onPositionChanged: { + var 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..13d0bd9 --- /dev/null +++ b/src/controls/private/globaltoolbar/AbstractPageHeader.qml @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.19 + +AbstractApplicationHeader { + id: root + // anchors.fill: parent + property Item container + property bool current + + minimumHeight: pageRow ? pageRow.globalToolBar.minimumHeight : Units.iconSizes.medium + Units.smallSpacing * 2 + maximumHeight: pageRow ? pageRow.globalToolBar.maximumHeight : minimumHeight + preferredHeight: pageRow ? pageRow.globalToolBar.preferredHeight : minimumHeight + + separatorVisible: pageRow ? pageRow.globalToolBar.separatorVisible : true + + Theme.colorSet: pageRow ? pageRow.globalToolBar.colorSet : Theme.Header + + leftPadding: pageRow ? (Math.min(Qt.application.layoutDirection === Qt.LeftToRight + ? Math.max(page.title.length > 0 ? Units.gridUnit : 0, pageRow.ScenePosition.x - page.ScenePosition.x + pageRow.globalToolBar.leftReservedSpace + Units.smallSpacing) + : Math.max(page.title.length > 0 ? Units.gridUnit : 0, -pageRow.width + pageRow.ScenePosition.x + page.ScenePosition.x + page.width + pageRow.globalToolBar.leftReservedSpace), + root.width/2)) + : Units.smallSpacing + + rightPadding: pageRow ? (Qt.application.layoutDirection === Qt.LeftToRight + ? Math.max(0, -pageRow.width - pageRow.ScenePosition.x + page.ScenePosition.x + page.width + pageRow.globalToolBar.rightReservedSpace) + : Math.max(0, pageRow.ScenePosition.x - page.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..18d95e1 --- /dev/null +++ b/src/controls/private/globaltoolbar/BreadcrumbControl.qml @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.19 as Kirigami + +Flickable { + id: root + + property Kirigami.PageRow pageRow: parent.pageRow + + readonly property Item currentItem: mainLayout.children[pageRow.currentIndex] + + contentHeight: height + contentWidth: mainLayout.width + clip: true + boundsBehavior: Flickable.StopAtBounds + interactive: Kirigami.Settings.hasTransientTouchInput + + contentX: Math.max(0, + Math.min(currentItem.x + currentItem.width/2 - root.width/2, + root.contentWidth - root.width)) + + RowLayout { + id: mainLayout + height: parent.height + spacing: 0 + Repeater { + id: mainRepeater + readonly property bool useLayers: pageRow.layers.depth > 1 + model: useLayers ? pageRow.layers.depth - 1 : pageRow.depth + delegate: MouseArea { + Layout.preferredWidth: delegateLayout.implicitWidth + Layout.fillHeight: true + onClicked: { + if (mainRepeater.useLayers) { + while (pageRow.layers.depth > modelData + 1) { + pageRow.layers.pop(); + } + } else { + pageRow.currentIndex = modelData; + } + } + hoverEnabled: !Kirigami.Settings.tabletMode + Rectangle { + color: Kirigami.Theme.highlightColor + anchors.fill: parent + radius: 3 + opacity: mainRepeater.count > 1 && parent.containsMouse ? 0.1 : 0 + } + RowLayout { + id: delegateLayout + anchors.fill: parent + // We can't use Kirigami.Page here instead of Item since we now accept pushing PageRow to a new layer + readonly property Item page: mainRepeater.useLayers ? pageRow.layers.get(modelData + 1) : pageRow.get(modelData) + spacing: 0 + + Kirigami.Icon { + visible: modelData > 0 + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: Kirigami.Units.iconSizes.small + Layout.preferredWidth: Layout.preferredHeight + isMask: true + color: Kirigami.Theme.textColor + source: LayoutMirroring.enabled ? "go-next-symbolic-rtl" : "go-next-symbolic" + } + Kirigami.Heading { + Layout.leftMargin: Kirigami.Units.largeSpacing + color: Kirigami.Theme.textColor + verticalAlignment: Text.AlignVCenter + wrapMode: Text.NoWrap + text: delegateLayout.page ? delegateLayout.page.title : "" + opacity: modelData === pageRow.currentIndex ? 1 : 0.4 + rightPadding: Kirigami.Units.largeSpacing + } + } + } + } + } + + Behavior on contentX { + NumberAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } +} diff --git a/src/controls/private/globaltoolbar/PageRowGlobalToolBarStyleGroup.qml b/src/controls/private/globaltoolbar/PageRowGlobalToolBarStyleGroup.qml new file mode 100644 index 0000000..ddd0f1d --- /dev/null +++ b/src/controls/private/globaltoolbar/PageRowGlobalToolBarStyleGroup.qml @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import org.kde.kirigami 2.19 as Kirigami + +QtObject { + id: globalToolBar + property int style: Kirigami.ApplicationHeaderStyle.None + onStyleChanged: if (style === Kirigami.ApplicationHeaderStyle.TabBar) { + console.warn("TabBar header style is deprecated.") + } + readonly property int actualStyle: { + if (style === Kirigami.ApplicationHeaderStyle.Auto) { + // TODO KF6 + // Legacy: if ApplicationHeader or ToolbarApplicationHeader are in the header or footer, disable the toolbar here + if (typeof applicationWindow !== "undefined" && applicationWindow().header && applicationWindow().header.toString().indexOf("ApplicationHeader") !== -1) { + return Kirigami.ApplicationHeaderStyle.None + } + + //non legacy logic + return (Kirigami.Settings.isMobile + ? (root.wideMode ? Kirigami.ApplicationHeaderStyle.Titles : Kirigami.ApplicationHeaderStyle.Breadcrumb) + : Kirigami.ApplicationHeaderStyle.ToolBar) + } + return style; + } + + // TODO KF6: remove bool support. + // Until then, `true` is considered as both `ShowBackButton | ShowForwardButton` together. + property var/*flags | bool*/ showNavigationButtons: (style !== Kirigami.ApplicationHeaderStyle.TabBar && (!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 +} diff --git a/src/controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml b/src/controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml new file mode 100644 index 0000000..eb11ede --- /dev/null +++ b/src/controls/private/globaltoolbar/PageRowGlobalToolBarUI.qml @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.19 as Kirigami +import "../../templates/private" as TemplatesPrivate +import "../" as Private + +Kirigami.AbstractApplicationHeader { + id: header + readonly property int leftReservedSpace: (buttonsLayout.visible && buttonsLayout.visibleChildren.length > 0 ? buttonsLayout.width : 0) + + (leftHandleAnchor.visible ? leftHandleAnchor.width : 0) + + (menuButton.visible ? menuButton.width : 0) + readonly property int rightReservedSpace: rightHandleAnchor.visible ? backButton.background.implicitHeight : 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 + + height: visible ? implicitHeight : 0 + minimumHeight: globalToolBar.minimumHeight + preferredHeight: globalToolBar.preferredHeight + maximumHeight: globalToolBar.maximumHeight + separatorVisible: globalToolBar.separatorVisible + + Kirigami.Theme.colorSet: globalToolBar.colorSet + Kirigami.Theme.textColor: currentItem ? currentItem.Kirigami.Theme.textColor : parent.Kirigami.Theme.textColor + + RowLayout { + anchors.fill: parent + spacing: 0 + + Item { + Layout.preferredWidth: applicationWindow().pageStack.globalToolBar.leftReservedSpace + visible: applicationWindow().pageStack !== root + } + + Item { + id: leftHandleAnchor + visible: (typeof applicationWindow() !== "undefined" && applicationWindow().globalDrawer && applicationWindow().globalDrawer.enabled && applicationWindow().globalDrawer.handleVisible && + applicationWindow().globalDrawer.handle.handleAnchor === leftHandleAnchor) && + (globalToolBar.canContainHandles || (breadcrumbLoader.pageRow.firstVisibleItem && + breadcrumbLoader.pageRow.firstVisibleItem.globalToolBarStyle === Kirigami.ApplicationHeaderStyle.ToolBar)) + + + Layout.preferredHeight: Math.min(backButton.implicitHeight, parent.height) + Layout.preferredWidth: height + } + + Private.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 : [] + } + } + + 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 + + // TODO KF6: make showNavigationButtons an int, and replace with strict === equality + visible: (globalToolBar.showNavigationButtons !== Kirigami.ApplicationHeaderStyle.NoNavigationButtons || applicationWindow().pageStack.layers.depth > 1) + && globalToolBar.actualStyle !== Kirigami.ApplicationHeaderStyle.None + + Layout.maximumWidth: visibleChildren.length > 0 ? Layout.preferredWidth : 0 + + TemplatesPrivate.BackButton { + id: backButton + Layout.leftMargin: leftHandleAnchor.visible ? 0 : Kirigami.Units.smallSpacing + Layout.minimumWidth: implicitHeight + Layout.minimumHeight: implicitHeight + Layout.maximumHeight: buttonsLayout.height + } + TemplatesPrivate.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.TabBar || globalToolBar.actualStyle === Kirigami.ApplicationHeaderStyle.Breadcrumb) && currentItem && currentItem.globalToolBarStyle !== Kirigami.ApplicationHeaderStyle.None + + // TODO: different implementation? + source: globalToolBar.actualStyle === Kirigami.ApplicationHeaderStyle.TabBar ? Qt.resolvedUrl("TabBarControl.qml") : Qt.resolvedUrl("BreadcrumbControl.qml") + } + + Item { + id: rightHandleAnchor + visible: (typeof applicationWindow() !== "undefined" && + applicationWindow().contextDrawer && + applicationWindow().contextDrawer.enabled && + applicationWindow().contextDrawer.handleVisible && + applicationWindow().contextDrawer.handle.handleAnchor === rightHandleAnchor && + (globalToolBar.canContainHandles || (breadcrumbLoader.pageRow && breadcrumbLoader.pageRow.lastVisibleItem && + breadcrumbLoader.pageRow.lastVisibleItem.globalToolBarStyle === Kirigami.ApplicationHeaderStyle.ToolBar))) + Layout.fillHeight: true + Layout.preferredWidth: height + } + } + background.opacity: breadcrumbLoader.active ? 1 : 0 +} diff --git a/src/controls/private/globaltoolbar/TabBarControl.qml b/src/controls/private/globaltoolbar/TabBarControl.qml new file mode 100644 index 0000000..42493d0 --- /dev/null +++ b/src/controls/private/globaltoolbar/TabBarControl.qml @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.19 as Kirigami + +/** + * @warning This will probably be deprecated in KF6. + */ +// TODO KF6 deprecated +Controls.TabBar { + id: root + property Kirigami.PageRow pageRow: parent.pageRow + + Repeater { + id: mainRepeater + model: pageRow.depth + delegate: Controls.TabButton { + anchors { + top:parent.top + bottom:parent.bottom + } + width: mainRepeater.count === 1 ? implicitWidth : Math.max(implicitWidth, Math.round(root.width/mainRepeater.count)) + height: root.height + readonly property Kirigami.Page page: pageRow.get(modelData) + text: page ? page.title : "" + checked: modelData === pageRow.currentIndex + onClicked: pageRow.currentIndex = modelData; + } + } +} diff --git a/src/controls/private/globaltoolbar/TitlesPageHeader.qml b/src/controls/private/globaltoolbar/TitlesPageHeader.qml new file mode 100644 index 0000000..ad62217 --- /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 2.15 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.19 + +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) + + asynchronous: true + sourceComponent: page ? page.titleDelegate : null + } +} diff --git a/src/controls/private/globaltoolbar/ToolBarPageHeader.qml b/src/controls/private/globaltoolbar/ToolBarPageHeader.qml new file mode 100644 index 0000000..963fc3c --- /dev/null +++ b/src/controls/private/globaltoolbar/ToolBarPageHeader.qml @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.19 +import "../" as Private + +AbstractPageHeader { + id: root + + implicitWidth: layout.implicitWidth + Units.smallSpacing * 2 + implicitHeight: Math.max(titleLoader.implicitHeight, toolBar.implicitHeight) + Units.smallSpacing * 2 + + MouseArea { + anchors.fill: parent + onPressed: { + page.forceActiveFocus() + mouse.accepted = false + } + } + + RowLayout { + id: layout + anchors.fill: parent + anchors.rightMargin: Units.smallSpacing + spacing: Units.smallSpacing + + Loader { + id: titleLoader + + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillWidth: item ? item.Layout.fillWidth : false + Layout.minimumWidth: item ? item.Layout.minimumWidth : -1 + Layout.preferredWidth: item ? item.Layout.preferredWidth : -1 + Layout.maximumWidth: item ? item.Layout.maximumWidth : -1 + + asynchronous: true + sourceComponent: page ? page.titleDelegate : null + } + + ActionToolBar { + id: toolBar + + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + Layout.fillHeight: true + + visible: actions.length > 0 + alignment: pageRow ? pageRow.globalToolBar.toolbarActionAlignment : Qt.AlignRight + heightMode: pageRow ? pageRow.globalToolBar.toolbarActionHeightMode : ToolBarLayout.ConstrainIfLarger + + actions: { + if (!page) { + return [] + } + + var result = [] + + if (page.actions.main) { + result.push(page.actions.main) + } + if (page.actions.left) { + result.push(page.actions.left) + } + if (page.actions.right) { + result.push(page.actions.right) + } + if (page.actions.contextualActions.length > 0) { + result = result.concat(Array.prototype.map.call(page.actions.contextualActions, function(item) { return item })) + } + + return result + } + + Binding { + target: page.actions.main + property: "displayHint" + value: page.actions.main ? (page.actions.main.displayHint | DisplayHint.KeepVisible) : null + } + Binding { + target: page.actions.left + property: "displayHint" + value: page.actions.left ? (page.actions.left.displayHint | DisplayHint.KeepVisible) : null + } + Binding { + target: page.actions.right + property: "displayHint" + value: page.actions.right ? (page.actions.right.displayHint | DisplayHint.KeepVisible) : null + } + } + } +} diff --git a/src/controls/qmldir b/src/controls/qmldir new file mode 100644 index 0000000..1afcd72 --- /dev/null +++ b/src/controls/qmldir @@ -0,0 +1,6 @@ +module org.kde.kirigami +plugin kirigamiplugin +classname KirigamiPlugin +depends QtQuick.Controls 2.0 +depends QtGraphicalEffects 1.0 +designersupported diff --git a/src/controls/settingscomponents/CategorizedSettings.qml b/src/controls/settingscomponents/CategorizedSettings.qml new file mode 100644 index 0000000..e281579 --- /dev/null +++ b/src/controls/settingscomponents/CategorizedSettings.qml @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: 2020 Tobias Fella + * SPDX-FileCopyrightText: 2021 Carl Schwan + * SPDX-FileCopyrightText: 2021 Felipe Kinoshita + * SPDX-FileCopyrightText: 2021 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.11 + +/** + * A container for setting actions showing them in a list view and displaying + * the actual page next to it. + * + * @since 5.86 + * @since org.kde.kirigami 2.18 + * @inherit kde::org::kirigami::PageRow + */ +PageRow { + id: pageSettingStack + + property list actions + property alias stack: pageSettingStack + property PagePool pool: PagePool {} + + readonly property string title: pageSettingStack.depth < 2 ? qsTr("Settings") : qsTr("Settings — %1").arg(pageSettingStack.get(1).title) + + bottomPadding: 0 + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + + columnView.columnWidth: Units.gridUnit * 7 // So it's the same size as the kxmlgui settings dialogs + globalToolBar.showNavigationButtons: ApplicationHeaderStyle.NoNavigationButtons + globalToolBar.style: Settings.isMobile ? ApplicationHeaderStyle.Breadcrumb : ApplicationHeaderStyle.None + + signal backRequested(var event) + onBackRequested: { + if (pageSettingStack.depth > 1 && !pageSettingStack.wideMode && pageSettingStack.currentIndex !== 0) { + event.accepted = true; + pageSettingStack.pop(); + } + } + onWidthChanged: if (pageSettingStack.depth < 2 && pageSettingStack.width >= Units.gridUnit * 40) { + actions[0].trigger(); + } + + initialPage: ScrollablePage { + title: qsTr("Settings") + bottomPadding: 0 + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + Theme.colorSet: Theme.View + ListView { + id: listview + Component.onCompleted: if (pageSettingStack.width >= Units.gridUnit * 40) { + actions[0].trigger(); + } else { + listview.currentIndex = -1; + } + model: pageSettingStack.actions + delegate: pageSettingStack.wideMode ? desktopStyle : mobileStyle + } + } + + Component { + id: desktopStyle + + QQC2.ItemDelegate { + width: parent && parent.width > 0 ? parent.width : implicitWidth + implicitWidth: contentItem.implicitWidth + Units.smallSpacing * 4 + implicitHeight: contentItem.implicitHeight + Units.smallSpacing * 2 + + action: modelData + highlighted: listview.currentIndex === index + onClicked: listview.currentIndex = index + contentItem: ColumnLayout { + spacing: Units.smallSpacing + + Icon { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Units.iconSizes.medium + Layout.preferredHeight: width + source: modelData.icon.name + } + + QQC2.Label { + Layout.fillWidth: true + Layout.leftMargin: Units.smallSpacing + Layout.rightMargin: Units.smallSpacing + text: modelData.text + wrapMode: Text.Wrap + color: highlighted ? Theme.highlightedTextColor : Theme.textColor + horizontalAlignment: Text.AlignHCenter + } + } + + } + } + + Component { + id: mobileStyle + + BasicListItem { + action: modelData + onClicked: { + listview.currentIndex = index; + } + } + } +} + diff --git a/src/controls/settingscomponents/SettingAction.qml b/src/controls/settingscomponents/SettingAction.qml new file mode 100644 index 0000000..12bc24f --- /dev/null +++ b/src/controls/settingscomponents/SettingAction.qml @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2021 Felipe Kinoshita + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.11 as Kirigami + +/** + * @brief SettingAction defines a settings page, and is typically used by a CategorizedSettings object. + * @since 5.86 + * @since org.kde.kirigami 2.18 + * @inherit org::kde::kirigami::PagePoolAction + */ +Kirigami.PagePoolAction { + pageStack: stack + pagePool: pool + basePage: stack.initialPage + + checkable: false +} diff --git a/src/controls/swipenavigator/PageTab.qml b/src/controls/swipenavigator/PageTab.qml new file mode 100644 index 0000000..a046b95 --- /dev/null +++ b/src/controls/swipenavigator/PageTab.qml @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: 2021 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 as QQC2 +import org.kde.kirigami 2.12 as Kirigami +import "templates" as T + +T.PageTab { + id: control + + implicitWidth: vertical ? verticalTitleRow.implicitWidth : horizontalTitleRow.implicitWidth + implicitHeight: vertical ? verticalTitleRow.implicitHeight : horizontalTitleRow.implicitHeight + + background: Rectangle { + border { + width: activeFocus ? 2 : 0 + color: Kirigami.Theme.textColor + } + color: { + if (control.active) { + return Kirigami.ColorUtils.adjustColor(Kirigami.Theme.activeTextColor, {"alpha": 0.2*255}) + } else if (control.needsAttention) { + return Kirigami.ColorUtils.adjustColor(Kirigami.Theme.negativeTextColor, {"alpha": 0.2*255}) + } else { + return "transparent" + } + } + } + + PrivateSwipeHighlight { + states: [ + State { name: "highlighted"; when: control.active }, + State { name: "requestingAttention"; when: control.needsAttention } + ] + } + + PrivateSwipeProgress { + anchors.fill: parent + visible: control.progress !== undefined + progress: control.progress + } + + RowLayout { + id: verticalTitleRow + anchors.fill: parent + Accessible.ignored: true + visible: vertical + + ColumnLayout { + Layout.margins: Kirigami.Settings.isMobile ? Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing + Layout.alignment: Qt.AlignCenter + + Kirigami.Icon { + visible: !!control.icon.name + source: control.icon.name + + Layout.preferredHeight: (control.presentation === T.PageTab.Presentation.Large) + ? Kirigami.Units.iconSizes.medium + : (Kirigami.Settings.isMobile ? Kirigami.Units.iconSizes.smallMedium : Kirigami.Units.iconSizes.small) + Layout.preferredWidth: Layout.preferredHeight + + Layout.alignment: (Qt.AlignHCenter | Qt.AlignBottom) + } + Kirigami.Heading { + level: (control.presentation === T.PageTab.Presentation.Large) ? 2 : 5 + text: control.title + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + + Layout.fillWidth: true + Layout.alignment: Qt.AlignCenter + } + } + } + + RowLayout { + id: horizontalTitleRow + anchors.fill: parent + Accessible.ignored: true + visible: !vertical + + RowLayout { + Layout.margins: (control.presentation === T.PageTab.Presentation.Large) ? Kirigami.Units.largeSpacing*2 : Kirigami.Units.largeSpacing + Layout.alignment: Qt.AlignVCenter + + Kirigami.Icon { + visible: !!control.icon.name + source: control.icon.name + + Layout.preferredHeight: (control.presentation === T.PageTab.Presentation.Large) + ? Kirigami.Units.iconSizes.medium + : (Kirigami.Settings.isMobile ? Kirigami.Units.iconSizes.smallMedium : Kirigami.Units.iconSizes.small) + Layout.preferredWidth: Layout.preferredHeight + + Layout.alignment: (Qt.AlignLeft | Qt.AlignVCenter) + } + Kirigami.Heading { + level: (control.presentation === T.PageTab.Presentation.Large) ? 1 : 2 + text: control.title + + Layout.fillWidth: true + Layout.alignment: (Qt.AlignLeft | Qt.AlignVCenter) + } + } + } + + Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter +} diff --git a/src/controls/swipenavigator/PrivateSwipeHighlight.qml b/src/controls/swipenavigator/PrivateSwipeHighlight.qml new file mode 100644 index 0000000..600cb61 --- /dev/null +++ b/src/controls/swipenavigator/PrivateSwipeHighlight.qml @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 +import org.kde.kirigami 2.12 as Kirigami + +Rectangle { + Accessible.ignored: true + + anchors { + bottom: Kirigami.Settings.isMobile ? undefined : parent.bottom + top: Kirigami.Settings.isMobile ? parent.top : undefined + left: parent.left + right: parent.right + } + + color: { + if (state === "highlighted") { + return Kirigami.Theme.activeTextColor + } else if (state === "requestingAttention") { + return Kirigami.Theme.negativeTextColor + } + return "transparent" + } + + // Unlike most things, we don't want to scale with the em grid, so we don't use a Unit. + height: 2 +} diff --git a/src/controls/swipenavigator/PrivateSwipeProgress.qml b/src/controls/swipenavigator/PrivateSwipeProgress.qml new file mode 100644 index 0000000..d3aaf05 --- /dev/null +++ b/src/controls/swipenavigator/PrivateSwipeProgress.qml @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 +import org.kde.kirigami 2.12 as Kirigami + +Item { + id: __progressRoot + property var progress + + Rectangle { + Accessible.ignored: true + + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + } + width: parent.width * __progressRoot.progress + color: Kirigami.ColorUtils.adjustColor(Kirigami.Theme.positiveTextColor, {"alpha": 0.2*255}) + + Rectangle { + anchors { + bottom: Kirigami.Settings.isMobile ? undefined : parent.bottom + top: Kirigami.Settings.isMobile ? parent.top : undefined + left: parent.left + right: parent.right + } + + color: Kirigami.Theme.positiveTextColor + + // Unlike most things, we don't want to scale with the em grid, so we don't use a Unit. + height: 2 + } + } + + + Rectangle { + Accessible.ignored: true + + anchors { + top: parent.top + bottom: parent.bottom + right: parent.right + } + width: parent.width - (parent.width * __progressRoot.progress) + color: Kirigami.ColorUtils.adjustColor(Kirigami.Theme.textColor, {"alpha": 0.1*255}) + + Rectangle { + anchors { + bottom: Kirigami.Settings.isMobile ? undefined : parent.bottom + top: Kirigami.Settings.isMobile ? parent.top : undefined + left: parent.left + right: parent.right + } + + color: Kirigami.ColorUtils.adjustColor(Kirigami.Theme.textColor, {"alpha": 0.1*255}) + + // Unlike most things, we don't want to scale with the em grid, so we don't use a Unit. + height: 2 + } + } +} \ No newline at end of file diff --git a/src/controls/swipenavigator/PrivateSwipeStack.qml b/src/controls/swipenavigator/PrivateSwipeStack.qml new file mode 100644 index 0000000..38ae420 --- /dev/null +++ b/src/controls/swipenavigator/PrivateSwipeStack.qml @@ -0,0 +1,100 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 +import org.kde.kirigami 2.12 as Kirigami + +StackView { + 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 + } + } + } +} diff --git a/src/controls/swipenavigator/PrivateSwipeTab.qml b/src/controls/swipenavigator/PrivateSwipeTab.qml new file mode 100644 index 0000000..5800a42 --- /dev/null +++ b/src/controls/swipenavigator/PrivateSwipeTab.qml @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 +import org.kde.kirigami 2.17 as Kirigami + +Kirigami.PageTab { + id: tabRoot + + active: index === columnView.currentIndex + title: modelData.title + progress: modelData.progress + needsAttention: modelData.needsAttention + icon: modelData.icon + + signal indexChanged(real xPos, real tabWidth) + + onActiveFocusChanged: { + if (activeFocus) { + tabRoot.indexChanged(tabRoot.x, tabRoot.width) + } + } + TapHandler { onTapped: columnView.currentIndex = index } + Connections { + target: columnView + function onCurrentIndexChanged() { + if (index === columnView.currentIndex) { + tabRoot.indexChanged(tabRoot.x, tabRoot.width) + } + } + } +} diff --git a/src/controls/swipenavigator/PrivateSwipeTabBar.qml b/src/controls/swipenavigator/PrivateSwipeTabBar.qml new file mode 100644 index 0000000..b3f292a --- /dev/null +++ b/src/controls/swipenavigator/PrivateSwipeTabBar.qml @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 + +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 +import org.kde.kirigami 2.14 as Kirigami + +ScrollView { + id: view + implicitWidth: bar.implicitWidth + ScrollBar.horizontal.visible: false + + Item { + height: view.height + implicitHeight: bar.implicitHeight + implicitWidth: bar.implicitWidth + width: Math.max(view.width, bar.implicitWidth) + + RowLayout { + id: bar + spacing: 0 + signal indexChanged(real xPos, real tabWidth) + + anchors.centerIn: parent + width: Kirigami.Settings.isMobile && swipeNavigatorRoot.height > swipeNavigatorRoot.width ? parent.width : implicitWidth + property real targetDestination + NumberAnimation { + id: scrollAni + target: view.ScrollBar.horizontal + property: "position" + to: bar.targetDestination + duration: Kirigami.Units.longDuration + easing.type: Easing.OutExpo + } + onIndexChanged: { + if (xPos > (bar.width)/2) { + bar.targetDestination = (1-view.ScrollBar.horizontal.size) * ((xPos+tabWidth) / bar.width) + scrollAni.restart() + } else { + bar.targetDestination = (1-view.ScrollBar.horizontal.size) * ((xPos) / bar.width) + scrollAni.restart() + } + } + + property Item layouter: Item { + Row { + id: expandedLayouter + Repeater { + model: swipeNavigatorRoot.pages + delegate: PrivateSwipeTab { vertical: false } + } + } + } + + Repeater { + model: swipeNavigatorRoot.pages + delegate: PrivateSwipeTab { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + vertical: Kirigami.Settings.isMobile + ? (swipeNavigatorRoot.width < swipeNavigatorRoot.height ? true : expandedLayouter.width > swipeNavigatorRoot.width) + : expandedLayouter.width > swipeNavigatorRoot.width + onIndexChanged: bar.indexChanged(xPos, tabWidth) + } + } + } + } +} diff --git a/src/controls/swipenavigator/SwipeNavigator.qml b/src/controls/swipenavigator/SwipeNavigator.qml new file mode 100644 index 0000000..cd10af6 --- /dev/null +++ b/src/controls/swipenavigator/SwipeNavigator.qml @@ -0,0 +1,331 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 +import org.kde.kirigami 2.13 as Kirigami + +/** + * @brief SwipeNavigator is a control providing for lateral navigation. + * @include swipenavigator/main.qml + * @inherit QtQuick.Item + */ +Item { + id: swipeNavigatorRoot + +//BEGIN properties + /** + * @brief This property holds the pages to swipe between. + */ + default property list pages + + /** + * @brief This property holds the StackView that is holding the core item, + * which allows users of SwipeNavigator to push pages on top of it. + * + * @property QtQuick.Controls.StackView stackView + */ + property alias layers: stackView + + /** + * @brief This property sets whether SwipeNavigator should be presented in large format, + * which is suitable for televisions. + * + * default: ``false`` + */ + property bool big: false + + /** + * @brief This property holds the item that will be displayed before the tabs. + * @property Item header + */ + property Component header: Item {visible: false} + + /** + * @brief This property holds the item that will be displayed after the tabs. + * @property Item footer + */ + property Component footer: Item {visible: false} + + /** + * @brief This property holds the initial tab index of the SwipeNavigator. + * + * default: ``0`` + */ + property int initialIndex: 0 + + /** + * @brief This property holds the currently displayed page in the SwipeNavigator. + * @property int currentIndex + */ + property alias currentIndex: columnView.currentIndex +//END properties + + /** + * @brief Pushes a page as a new dialog on desktop and as a layer on mobile. + * @param page The page can be defined as a component, item or string. If an item is + * used then the page will get re-parented. If a string is used then it + * is interpreted as a url that is used to load a page component. + * @param properties The properties given when initializing the page. + * @param windowProperties The properties given to the initialized window on desktop. + * @return The newly created page + */ + function pushDialogLayer(page, properties = {}, windowProperties = {}) { + let item; + if (Settings.isMobile) { + item = layers.push(page, properties); + } else { + const windowComponent = Qt.createComponent(Qt.resolvedUrl("./ApplicationWindow.qml")); + if (!windowProperties.modality) { + windowProperties.modality = Qt.WindowModal; + } + if (!windowProperties.height) { + windowProperties.height = Units.gridUnit * 30; + } + if (!windowProperties.width) { + windowProperties.width = Units.gridUnit * 50; + } + if (!windowProperties.minimumWidth) { + windowProperties.minimumWidth = Units.gridUnit * 20; + } + if (!windowProperties.minimumHeight) { + windowProperties.minimumHeight = Units.gridUnit * 15; + } + if (!windowProperties.flags) { + windowProperties.flags = Qt.Dialog | Qt.WindowCloseButtonHint; + } + const window = windowComponent.createObject(swipeNavigatorRoot, windowProperties); + item = window.pageStack.push(page, properties); + } + item.Keys.escapePressed.connect(function() { item.closeDialog() }); + return item; + } + + implicitWidth: stackView.implicitWidth + implicitHeight: stackView.implicitHeight + + QtObject { + id: _gridManager + readonly property bool tall: (_header.width + __main.implicitWidth + Math.abs(__main.offset) + _footer.width) > swipeNavigatorRoot.width + readonly property int rowOne: Kirigami.Settings.isMobile ? 1 : 0 + readonly property int rowTwo: Kirigami.Settings.isMobile ? 0 : 1 + readonly property int rowDirection: Kirigami.Settings.isMobile ? 1 : -1 + property Item item: Item { + states: [ + State { + name: "small" + when: !_gridManager.tall + }, + State { + name: "tall" + when: _gridManager.tall + } + ] + transitions: [ + Transition { + to: "tall" + ScriptAction { + script: { + // Let's take these out of the layout first... + _dummyOne.visible = false + _dummyTwo.visible = false + // Now we move the header and footer up + _header.Layout.row += _gridManager.rowDirection + _footer.Layout.row += _gridManager.rowDirection + // Now that the header and footer are out of the way, + // let's expand the tabs + __main.Layout.column-- + __main.Layout.columnSpan = 3 + } + } + }, + Transition { + to: "small" + ScriptAction { + script: { + // Let's move the tabs back to where they belong + __main.Layout.columnSpan = 1 + __main.Layout.column++ + // Move the header and footer down into the empty space + _header.Layout.row -= _gridManager.rowDirection + _footer.Layout.row -= _gridManager.rowDirection + + // Now we can bring these guys back in + _dummyOne.visible = false + _dummyTwo.visible = false + } + } + } + ] + } + } + + + StackView { + id: stackView + + anchors.fill: parent + + function clear() { + // don't let it kill the main page row + var d = stackView.depth; + for (var i = 1; i < d; ++i) { + pop(); + } + } + + initialItem: TabViewLayout { + bar: ToolBar { + id: topToolBar + + padding: 0 + bottomPadding: 1 + + GridLayout { + id: _grid + + rowSpacing: 0 + columnSpacing: 0 + anchors.fill: parent + rows: 2 + columns: 3 + + // Row one + Item { id: _spacer; Layout.row: 0; Layout.column: 1; Layout.fillWidth: true } + Item { id: _dummyOne; Layout.row: 0; Layout.column: 0 } + Item { id: _dummyTwo; Layout.row: 0; Layout.column: 2 } + + // Row two + Loader { id: _header; sourceComponent: swipeNavigatorRoot.header; Layout.row: 1; Layout.column: 0 } + PrivateSwipeTabBar { + id: __main + readonly property int offset: _header.width - _footer.width + readonly property int effectiveOffset: _gridManager.tall ? 0 : offset + Layout.rightMargin: effectiveOffset > 0 ? effectiveOffset : 0 + Layout.leftMargin: effectiveOffset < 0 ? -effectiveOffset : 0 + Layout.fillHeight: true + Layout.fillWidth: true//Kirigami.Settings.isMobile && swipeNavigatorRoot.height > swipeNavigatorRoot.width + Layout.alignment: Qt.AlignHCenter + Layout.row: 1 + Layout.column: 1 + + } + Loader { id: _footer; sourceComponent: swipeNavigatorRoot.footer; Layout.row: 1; Layout.column: 2 } + } + + Accessible.role: Accessible.PageTabList + } + contentItem: Kirigami.ColumnView { + id: columnView + Layout.fillWidth: true + Layout.fillHeight: true + Layout.row: Kirigami.Settings.isMobile ? 0 : 1 + + columnResizeMode: Kirigami.ColumnView.SingleColumn + + contentChildren: swipeNavigatorRoot.pages + + Component.onCompleted: { + columnView.currentIndex = swipeNavigatorRoot.initialIndex + } + // We only want the current page to be focusable, so we + // disable the inactive pages. + onCurrentIndexChanged: { + Array.from(swipeNavigatorRoot.pages).forEach(item => item.enabled = false) + swipeNavigatorRoot.pages[currentIndex].enabled = true + } + } + } + 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 + } + } + } + } +} diff --git a/src/controls/swipenavigator/TabViewLayout.qml b/src/controls/swipenavigator/TabViewLayout.qml new file mode 100644 index 0000000..4bbb560 --- /dev/null +++ b/src/controls/swipenavigator/TabViewLayout.qml @@ -0,0 +1,62 @@ +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 +import org.kde.kirigami 2.12 as Kirigami + +/** + * Control for dynamically moving a bar above or below a content item, + * e.g. to move tabs to the bottom on mobile. + * + * @inherit QtQuick.Item + */ +Item { + id: __root + + /** + * @brief Position options for TabViewLayout. + */ + enum Position { + Top, + Bottom + } + + /** + * @brief The position of the bar in relation to the contentItem. + * + * default: `Position.Bottom on mobile, and Position.Top otherwise` + * + * @see org::kde::kirigami::TabViewLayout::Position + */ + property int position: Kirigami.Settings.isMobile ? TabViewLayout.Position.Bottom : TabViewLayout.Position.Top + + required property Item bar + onBarChanged: { + bar.parent = __grid + bar.Layout.row = Qt.binding(() => (__root.position === TabViewLayout.Position.Bottom) ? 1 : 0) + bar.Layout.fillWidth = true + if (bar instanceof ToolBar) { + bar.position = Qt.binding(() => (__root.position === TabViewLayout.Position.Bottom) ? ToolBar.Footer : ToolBar.Header) + } + } + + required property Item contentItem + onContentItemChanged: { + contentItem.parent = __grid + contentItem.Layout.row = Qt.binding(() => (__root.position === TabViewLayout.Position.Bottom) ? 0 : 1) + contentItem.Layout.fillWidth = true + contentItem.Layout.fillHeight = true + } + + implicitWidth: __grid.implicitWidth + implicitHeight: __grid.implicitHeight + + GridLayout { + id: __grid + children: [__root.bar, __root.contentItem] + + rowSpacing: 0 + columns: 1 + + anchors.fill: parent + } +} diff --git a/src/controls/swipenavigator/templates/PageTab.qml b/src/controls/swipenavigator/templates/PageTab.qml new file mode 100644 index 0000000..6a2165e --- /dev/null +++ b/src/controls/swipenavigator/templates/PageTab.qml @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2021 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 as QQC2 +import org.kde.kirigami 2.12 as Kirigami + +import "../../private" as Private + +// TODO?: refactor into abstractbutton +QQC2.Control { + id: control + + enum Presentation { + Normal, + Large + } + + property string title + property bool active + + property Private.ActionIconGroup icon: Private.ActionIconGroup {} + property int presentation: PageTab.Presentation.Normal + property bool vertical: false + property var progress // type: real? + property bool needsAttention: false + + activeFocusOnTab: true + + Accessible.name: control.title + Accessible.description: { + if (!!control.progress) { + if (control.active) { + //: Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign. + return qsTr("Current page. Progress: %1 percent.").arg(Math.round(control.progress*100)) + } else { + //: Accessibility text for a page tab. Keep the text as concise as possible. + return qsTr("Navigate to %1. Progress: %2 percent.").arg(control.title).arg(Math.round(control.progress*100)) + } + } else { + if (control.active) { + //: Accessibility text for a page tab. Keep the text as concise as possible. + return qsTr("Current page.") + } else if (control.needsAttention) { + //: Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. + return qsTr("Navigate to %1. Demanding attention.", control.title) + } else { + //: Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible. + return qsTr("Navigate to %1.", control.title) + } + } + } + Accessible.role: Accessible.PageTab + Accessible.focusable: true + Accessible.onPressAction: control.clicked() + + Keys.onPressed: { + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { + control.clicked() + } + } +} diff --git a/src/controls/templates/AbstractApplicationHeader.qml b/src/controls/templates/AbstractApplicationHeader.qml new file mode 100644 index 0000000..41af9eb --- /dev/null +++ b/src/controls/templates/AbstractApplicationHeader.qml @@ -0,0 +1,199 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.7 +import QtQuick.Layouts 1.2 +import "private" +import org.kde.kirigami 2.14 +import QtQuick.Controls 2.4 as Controls + +/** + * @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 + * * preferredHeight: 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 + property int preferredHeight: Math.max(...(Array.from(mainItem.children).map(elm => elm.implicitHeight))) + topPadding + bottomPadding + property int maximumHeight: Units.gridUnit * 3 + + property int position: Controls.ToolBar.Header + + property PageRow pageRow: __appWindow ? __appWindow.pageStack: null + property Page page: pageRow ? pageRow.currentItem : 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 + // whether or not the header should be + // "pushed" back when scrolling using the + // touch screen + property bool hideWhenTouchScrolling: root.pageRow ? root.pageRow.globalToolBar.hideWhenTouchScrolling : false + + LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft + LayoutMirroring.childrenInherit: true + + 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 + + onPageChanged: { + // NOTE: The Connections object doesn't work with attached properties signals, so we have to do this by hand + if (headerItem.oldPage) { + headerItem.oldPage.ColumnView.scrollIntention.disconnect(headerItem.scrollIntentHandler); + } + if (root.page) { + root.page.ColumnView.scrollIntention.connect(headerItem.scrollIntentHandler); + } + headerItem.oldPage = root.page; + } + Component.onDestruction: { + if (root.page) { + root.page.ColumnView.scrollIntention.disconnect(headerItem.scrollIntentHandler); + } + } + + NumberAnimation { + id: heightAnim + target: root + property: "implicitHeight" + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + Connections { + target: __appWindow + function onControlsVisibleChanged() { + heightAnim.from = root.implicitHeight + heightAnim.to = __appWindow.controlsVisible ? root.preferredHeight : 0; + heightAnim.restart(); + } + } + + Item { + id: headerItem + anchors { + left: parent.left + right: parent.right + bottom: !Settings.isMobile || root.position === Controls.ToolBar.Header ? parent.bottom : undefined + top: !Settings.isMobile || root.position === Controls.ToolBar.Footer ? parent.top : undefined + } + + height: __appWindow && __appWindow.reachableMode && __appWindow.reachableModeEnabled ? root.maximumHeight : (root.minimumHeight > 0 ? Math.max(root.height, root.minimumHeight) : Math.max(root.height, root.preferredHeight)) + + function scrollIntentHandler(event) { + if (!root.hideWhenTouchScrolling) { + return + } + + if (root.pageRow + && root.pageRow.globalToolBar.actualStyle !== ApplicationHeaderStyle.TabBar + && root.pageRow.globalToolBar.actualStyle !== 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; + } + } + + property Page oldPage + + Connections { + target: root.page ? root.page.globalToolBarItem : null + enabled: target + function onImplicitHeightChanged() { root.implicitHeight = root.page.globalToolBarItem.implicitHeight } + } + + Timer { + id: slideResetTimer + interval: 500 + onTriggered: { + if ((root.pageRow ? root.pageRow.wideMode : (__appWindow && __appWindow.wideScreen)) || !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: Array.from(children).forEach(item => item.anchors.verticalCenter = this.verticalCenter) + anchors { + fill: parent + leftMargin: root.leftPadding + topMargin: root.topPadding + 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..ea27c91 --- /dev/null +++ b/src/controls/templates/AbstractCard.qml @@ -0,0 +1,171 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.6 +import QtQuick.Layouts 1.2 +import QtQuick.Templates 2.0 as T +import org.kde.kirigami 2.4 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 Item 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 Item 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(background.implicitWidth, mainLayout.implicitWidth) + leftPadding + rightPadding + implicitHeight: mainLayout.implicitHeight + topPadding + bottomPadding + + hoverEnabled: !Kirigami.Settings.tabletMode && showClickFeedback + // if it's in a CardLayout, try to expand horizontal cards to both columns + Layout.columnSpan: headerOrientation === Qt.Horizontal && parent.hasOwnProperty("columns") ? parent.columns : 1 + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.View + + topPadding: contentItemParent.children.length > 0 ? Kirigami.Units.largeSpacing : 0 + leftPadding: Kirigami.Units.largeSpacing + bottomPadding: contentItemParent.children.length > 0 ? Kirigami.Units.largeSpacing : 0 + rightPadding: Kirigami.Units.largeSpacing + + width: ListView.view ? ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin : undefined + + GridLayout { + id: mainLayout + rowSpacing: root.topPadding + columnSpacing: root.leftPadding + anchors { + top: parent.top + left: parent.left + right: parent.right + leftMargin: root.leftPadding + topMargin: root.topPadding + rightMargin: root.rightPadding + bottom: parent.bottom + bottomMargin: root.bottomPadding + } + columns: headerOrientation === Qt.Vertical ? 1 : 2 + function preferredHeight(item) { + if (!item) { + return 0; + } + if (item.Layout.preferredHeight > 0) { + return item.Layout.preferredHeight; + } + return item.implicitHeight + } + Item { + id: headerParent + Layout.fillWidth: true + Layout.fillHeight: root.headerOrientation === Qt.Horizontal + Layout.rowSpan: root.headerOrientation === Qt.Vertical ? 1 : 2 + Layout.preferredWidth: header ? header.implicitWidth : 0 + Layout.preferredHeight: root.headerOrientation === Qt.Vertical ? mainLayout.preferredHeight(header) : -1 + visible: children.length > 0 + } + Item { + id: contentItemParent + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: root.topPadding + Layout.bottomMargin: root.bottomPadding + Layout.preferredWidth: contentItem ? contentItem.implicitWidth : 0 + Layout.preferredHeight: mainLayout.preferredHeight(contentItem) + visible: children.length > 0 + } + Item { + id: footerParent + Layout.fillWidth: true + Layout.preferredWidth: footer ? footer.implicitWidth : 0 + Layout.preferredHeight: mainLayout.preferredHeight(footer) + visible: children.length > 0 + } + } + +//BEGIN signal handlers + onContentItemChanged: { + if (!contentItem) { + return; + } + + contentItem.parent = contentItemParent; + contentItem.anchors.fill = contentItemParent; + } + onHeaderChanged: { + if (!header) { + return; + } + + header.parent = headerParent; + header.anchors.fill = headerParent; + } + onFooterChanged: { + if (!footer) { + return; + } + + //make the footer always looking it's at the bottom of the card + footer.parent = footerParent; + footer.anchors.left = footerParent.left; + footer.anchors.top = footerParent.top; + footer.anchors.right = footerParent.right; + footer.anchors.topMargin = Qt.binding(function() {return (root.height - root.bottomPadding - root.topPadding) - (footerParent.y + footerParent.height)}); + } + Component.onCompleted: { + contentItemChanged(); + } +//END signal handlers +} diff --git a/src/controls/templates/AbstractChip.qml b/src/controls/templates/AbstractChip.qml new file mode 100644 index 0000000..3008a34 --- /dev/null +++ b/src/controls/templates/AbstractChip.qml @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2022 Felipe Kinoshita +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Templates 2.15 as T + +/** + * @brief AbstractChip 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 signal is emitted when the close button has been clicked. + */ + signal removed() +} diff --git a/src/controls/templates/AbstractListItem.qml b/src/controls/templates/AbstractListItem.qml new file mode 100644 index 0000000..7135827 --- /dev/null +++ b/src/controls/templates/AbstractListItem.qml @@ -0,0 +1,195 @@ +/* + * SPDX-FileCopyrightText: 2010 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.0 +import org.kde.kirigami 2.4 +// NOTE: This must stay at 2.2 until KF6 due to retrocompatibility of the "icon" property +import QtQuick.Templates 2.2 as T2 +import QtQuick.Templates 2.4 as QQC2 + +/** + * @brief An item delegate for the primitive ListView component. + * + * It's intended to make all listviews look coherent. + * + * @inherit QtQuick.Controls.ItemDelegate + */ +T2.ItemDelegate { + id: listItem + +//BEGIN properties + /** + * @brief This property sets whether the item should emit signals related to mouse interaction. + * + * default: ``true`` + * + * @deprecated This will be removed in KF6. + */ + property bool supportsMouseEvents: 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 + */ + 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: ``true`` + */ + property bool separatorVisible: true + + /** + * @brief This property holds list item's background color. + * + * It is advised to use the default value. + * default: ``"transparent"`` + */ + property color backgroundColor: "transparent" + + /** + * @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: 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: 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 an AbstractListItem, + * their color will have to be manually set with this property. + */ + property color textColor: 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 an AbstractListItem, + * their color will have to be manually set with this property. + */ + property color activeTextColor: Theme.highlightedTextColor + + default property alias _default: listItem.contentItem + + // NOTE: Overrides action property of newer import versions which we can't use + /** + * @brief This property holds the item action. + * @property QtQuick.Controls.Action action + */ + property QQC2.Action action +//END properties + + activeFocusOnTab: ListView.view ? false : true + + text: action ? action.text : undefined + checked: action ? action.checked : false + checkable: action ? action.checkable : false + onClicked: { + if (ListView.view && typeof index !== "undefined") { + ListView.view.currentIndex = index; + } + if (!action) { + return; + } + + action.trigger(); + checked = Qt.binding(function() { return action.checked }); + } + //Theme.inherit: false + //Theme.colorSet: Theme.View + + padding: Settings.tabletMode ? Units.largeSpacing : Units.smallSpacing + + leftPadding: padding*2 + topPadding: padding + + rightPadding: padding*2 + bottomPadding: padding + + implicitWidth: contentItem ? contentItem.implicitWidth + leftPadding + rightPadding : Units.gridUnit * 12 + + implicitHeight: contentItem.implicitHeight + topPadding + bottomPadding + + width: parent && parent.width > 0 ? parent.width : implicitWidth + Layout.fillWidth: true + + opacity: enabled ? 1 : 0.6 + + height: implicitHeight + + onVisibleChanged: { + if (visible) { + height = Qt.binding(() => { return implicitHeight; }) + } else { + if (ListView.view && ListView.view.visible) { + height = 0; + } + } + } + + hoverEnabled: true + + QtObject { + id: internal + property Flickable view: listItem.ListView.view || (listItem.parent ? listItem.parent.ListView.view : null) + property bool indicateActiveFocus: listItem.pressed || Settings.tabletMode || listItem.activeFocus || (view ? view.activeFocus : false) + } + + Accessible.role: Accessible.ListItem + highlighted: focus && ListView.isCurrentItem && ListView.view && ListView.view.keyNavigationEnabled +} diff --git a/src/controls/templates/ApplicationHeader.qml b/src/controls/templates/ApplicationHeader.qml new file mode 100644 index 0000000..ae94c6b --- /dev/null +++ b/src/controls/templates/ApplicationHeader.qml @@ -0,0 +1,388 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import QtQuick.Controls 2.0 as QQC2 +import QtQuick.Layouts 1.2 +import "private" +import org.kde.kirigami 2.4 + + +/** + * @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 to the same value. + */ +AbstractApplicationHeader { + id: header + +//BEGIN properties + /** + * @brief This property sets the way the separator between pages should be drawn in the header. + * + * Allowed values are: + * * ``Kirigami.ApplicationHeaderStyle.Breadcrumb``: The pages are hierarchical, separated by an arrow. + * * ``Kirigami.ApplicationHeaderStyle.TabBar``: The pages are intended to behave like pages of a tabbed view. + * and the separator will look limke a dot. + * + * When the header is in wide screen mode, no separator will be drawn. + * + * default: ``ApplicationHeaderStyle.Auto`` + */ + property int headerStyle: ApplicationHeaderStyle.Auto + + /** + * @brief This property sets whether the back button is enabled. + * + * default: `when true, there will be a back button present that will make the pagerow scroll back when clicked` + */ + property bool backButtonEnabled: (!titleList.isTabBar && (!Settings.isMobile || Qt.platform.os === "ios")) + + property Component pageDelegate: Component { + Row { + height: parent.height + + spacing: Units.smallSpacing + + x: Units.smallSpacing + + Icon { + // in tabbar mode this is just a spacer + visible: !titleList.wideMode && ((typeof modelData !== "undefined" && modelData > 0) || titleList.internalHeaderStyle === ApplicationHeaderStyle.TabBar) + anchors.verticalCenter: parent.verticalCenter + height: Units.iconSizes.small + width: height + selected: header.background && header.background.color && header.background.color === Theme.highlightColor + source: titleList.isTabBar ? "" : (LayoutMirroring.enabled ? "go-next-symbolic-rtl" : "go-next-symbolic") + } + + Heading { + id: title + width: Math.min(parent.width, Math.min(titleList.width, implicitWidth)) + Units.smallSpacing + anchors.verticalCenter: parent.verticalCenter + opacity: current ? 1 : 0.4 + // Scaling animate NativeRendering is too slow + renderType: Text.QtRendering + color: header.background && header.background.color && header.background.color === Theme.highlightColor ? Theme.highlightedTextColor : Theme.textColor + elide: Text.ElideRight + text: page ? page.title : "" + font.pointSize: -1 + font.pixelSize: Math.max(1, titleList.height * 0.7) + verticalAlignment: Text.AlignVCenter + wrapMode: Text.NoWrap + Rectangle { + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + height: Units.smallSpacing + color: title.color + opacity: 0.6 + visible: titleList.isTabBar && current + } + } + } + } +//END properties + +//BEGIN signal handlers + onBackButtonEnabledChanged: { + if (backButtonEnabled && !titleList.backButton) { + var component = Qt.createComponent(Qt.resolvedUrl("private/BackButton.qml")); + titleList.backButton = component.createObject(navButtons); + component = Qt.createComponent(Qt.resolvedUrl("private/ForwardButton.qml")); + titleList.forwardButton = component.createObject(navButtons, {"headerFlickable": titleList}); + } else if (titleList.backButton) { + titleList.backButton.destroy(); + titleList.forwardButton.destroy(); + } + } + + Component.onCompleted: print("Warning: ApplicationHeader is deprecated, remove and use the automatic internal toolbar instead.") +//END signal handlers + + Rectangle { + anchors { + verticalCenter: parent.verticalCenter + } + visible: titleList.x > 0 && !titleList.atXBeginning + height: parent.height * 0.7 + color: Theme.highlightedTextColor + width: Math.ceil(Units.smallSpacing / 6) + opacity: 0.4 + } + + QQC2.StackView { + id: stack + anchors { + fill: parent + leftMargin: navButtons.width + rightMargin: __appWindow.contextDrawer && __appWindow.contextDrawer.handleVisible && __appWindow.contextDrawer.handle && __appWindow.contextDrawer.handle.y === 0 ? __appWindow.contextDrawer.handle.width : 0 + } + initialItem: titleList + + popEnter: Transition { + YAnimator { + from: -height + to: 0 + duration: Units.longDuration + easing.type: Easing.OutCubic + } + } + popExit: Transition { + YAnimator { + from: 0 + to: height + duration: Units.longDuration + easing.type: Easing.OutCubic + } + } + + pushEnter: Transition { + YAnimator { + from: height + to: 0 + duration: Units.longDuration + easing.type: Easing.OutCubic + } + } + + pushExit: Transition { + YAnimator { + from: 0 + to: -height + duration: Units.longDuration + easing.type: Easing.OutCubic + } + } + + replaceEnter: Transition { + YAnimator { + from: height + to: 0 + duration: Units.longDuration + easing.type: Easing.OutCubic + } + } + + replaceExit: Transition { + YAnimator { + from: 0 + to: -height + duration: Units.longDuration + easing.type: Easing.OutCubic + } + } + } + Separator { + id: separator + height: parent.height * 0.6 + visible: navButtons.width > 0 + anchors { + verticalCenter: parent.verticalCenter + left: navButtons.right + } + } + Separator { + height: parent.height * 0.6 + visible: stack.anchors.rightMargin > 0 + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + rightMargin: stack.anchors.rightMargin + } + } + Repeater { + model: pageRow.layers.depth -1 + delegate: Loader { + asynchronous: true + sourceComponent: header.pageDelegate + readonly property Page page: pageRow.layers.get(modelData+1) + readonly property bool current: true; + Component.onCompleted: stack.push(this) + Component.onDestruction: stack.pop() + } + } + + Row { + id: navButtons + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + topMargin: Units.smallSpacing + bottomMargin: Units.smallSpacing + } + Item { + height: parent.height + width: (applicationWindow().header && applicationWindow().header.toString().indexOf("ToolBarApplicationHeader") === 0) && __appWindow.globalDrawer && __appWindow.globalDrawer.handleVisible && __appWindow.globalDrawer.handle && __appWindow.globalDrawer.handle.y === 0 ? __appWindow.globalDrawer.handle.width : 0 + } + } + + Flickable { + id: titleList + readonly property bool wideMode: pageRow.hasOwnProperty("wideMode") ? pageRow.wideMode : __appWindow.wideScreen + property int internalHeaderStyle: header.headerStyle === ApplicationHeaderStyle.Auto ? (titleList.wideMode ? ApplicationHeaderStyle.Titles : ApplicationHeaderStyle.Breadcrumb) : header.headerStyle + // if scrolling the titlebar should scroll also the pages and vice versa + property bool scrollingLocked: (header.headerStyle === ApplicationHeaderStyle.Titles || titleList.wideMode) + //uses this to have less strings comparisons + property bool scrollMutex + property bool isTabBar: header.headerStyle === ApplicationHeaderStyle.TabBar + + property Item backButton + property Item forwardButton + clip: true + + + boundsBehavior: Flickable.StopAtBounds + readonly property alias model: mainRepeater.model + contentWidth: contentItem.width + contentHeight: height + + readonly property int currentIndex: pageRow && pageRow.currentIndex !== undefined ? pageRow.currentIndex : 0 + readonly property int count: mainRepeater.count + + function gotoIndex(idx) { + // don't actually scroll in widescreen mode + if (titleList.wideMode || contentItem.children.length < 2) { + return; + } + listScrollAnim.running = false + var pos = titleList.contentX; + var destPos; + titleList.contentX = Math.max(((contentItem.children[idx] || {x: 0}).x + (contentItem.children[idx] || {width: 0}).width) - titleList.width, Math.min(titleList.contentX, (contentItem.children[idx] || {x: 0}).x)); + destPos = titleList.contentX; + listScrollAnim.from = pos; + listScrollAnim.to = destPos; + listScrollAnim.running = true; + } + + NumberAnimation { + id: listScrollAnim + target: titleList + property: "contentX" + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + Timer { + id: contentXSyncTimer + interval: 0 + onTriggered: { + titleList.contentX = pageRow.contentItem.contentX - pageRow.contentItem.originX + titleList.originX; + } + } + onCountChanged: contentXSyncTimer.restart(); + onCurrentIndexChanged: gotoIndex(currentIndex); + onModelChanged: gotoIndex(currentIndex); + onContentWidthChanged: gotoIndex(currentIndex); + + onContentXChanged: { + if (movingHorizontally && !titleList.scrollMutex && titleList.scrollingLocked && !pageRow.contentItem.moving) { + titleList.scrollMutex = true; + pageRow.contentItem.contentX = titleList.contentX - titleList.originX + pageRow.contentItem.originX; + titleList.scrollMutex = false; + } + } + onHeightChanged: { + titleList.returnToBounds() + } + onMovementEnded: { + if (titleList.scrollingLocked) { + //this will trigger snap as well + pageRow.contentItem.flick(0,0); + } + } + onFlickEnded: movementEnded(); + + NumberAnimation { + id: scrollTopAnimation + target: pageRow.currentItem && pageRow.currentItem.flickable ? pageRow.currentItem.flickable : null + property: "contentY" + to: 0 + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + + Row { + id: contentItem + spacing: 0 + Repeater { + id: mainRepeater + model: pageRow.depth + delegate: MouseArea { + id: delegate + readonly property int currentIndex: index + readonly property var currentModelData: modelData + clip: true + + width: { + // more columns shown? + if (titleList.scrollingLocked && delegateLoader.page) { + return delegateLoader.page.width - (index === 0 ? navButtons.width : 0) - (index === pageRow.depth-1 ? stack.anchors.rightMargin : 0); + } else { + return Math.min(titleList.width, delegateLoader.implicitWidth + Units.smallSpacing); + } + } + + height: titleList.height + onClicked: { + if (pageRow.currentIndex === modelData) { + // scroll up if current otherwise make current + if (!pageRow.currentItem.flickable) { + return; + } + if (pageRow.currentItem.flickable.contentY > -__appWindow.header.height) { + scrollTopAnimation.to = -pageRow.currentItem.flickable.topMargin; + scrollTopAnimation.running = true; + } + + } else { + pageRow.currentIndex = modelData; + } + } + + Loader { + id: delegateLoader + height: parent.height + x: titleList.wideMode || headerStyle === ApplicationHeaderStyle.Titles ? (Math.min(delegate.width - implicitWidth, Math.max(0, titleList.contentX - delegate.x))) : 0 + width: parent.width - x + + Connections { + target: delegateLoader.page.Component + function onDestruction() { delegateLoader.sourceComponent = null } + } + + sourceComponent: header.pageDelegate + + readonly property Page page: pageRow.get(modelData) + // NOTE: why not use ListViewCurrentIndex? because listview itself resets + // currentIndex in some situations (since here we are using an int as a model, + // even more often) so the property binding gets broken + readonly property bool current: pageRow.currentIndex === index + readonly property int index: parent.currentIndex + readonly property var modelData: parent.currentModelData + } + } + } + } + Connections { + target: titleList.scrollingLocked ? pageRow.contentItem : null + function onContentXChanged() { + if (!titleList.dragging && !titleList.movingHorizontally && !titleList.scrollMutex) { + titleList.contentX = pageRow.contentItem.contentX - pageRow.contentItem.originX + titleList.originX; + } + } + } + } +} diff --git a/src/controls/templates/InlineMessage.qml b/src/controls/templates/InlineMessage.qml new file mode 100644 index 0000000..fb05bc4 --- /dev/null +++ b/src/controls/templates/InlineMessage.qml @@ -0,0 +1,302 @@ +/* + * SPDX-FileCopyrightText: 2018 Eike Hein + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.7 +import QtQuick.Templates 2.0 as T2 +import QtQuick.Controls 2.0 as Controls +import QtQuick.Layouts 1.0 +import org.kde.kirigami 2.5 as Kirigami +import "private" + +/** + * 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 + * InlineMessage { + * type: Kirigami.MessageType.Error + * + * text: "My error message" + * + * actions: [ + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * }, + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * } + * ] + * } + * @endcode + * + * @since 5.45 + * @inherit QtQuick.Controls.Control + */ +T2.Control { + id: root + + visible: false + + /** + * 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 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 IconPropertiesGroup icon: 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: root.hasOwnProperty("_animating") && _animating + + implicitHeight: visible ? contentLayout.implicitHeight + (2 * (background.border.width + Kirigami.Units.smallSpacing)) : 0 + + property bool _animating: false + + leftPadding: background.border.width + Kirigami.Units.smallSpacing + topPadding: background.border.width + Kirigami.Units.smallSpacing + rightPadding: background.border.width + Kirigami.Units.smallSpacing + bottomPadding: background.border.width + Kirigami.Units.smallSpacing + + 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 (actionsLayout.atBottom) { + return text.implicitHeight + actionsLayout.height + Kirigami.Units.gridUnit + } else { + return Math.max(icon.implicitHeight, text.implicitHeight, closeButton.implicitHeight, actionsLayout.height) + } + } + + readonly property int remainingWidth: width - (text.implicitWidth + icon.width + Kirigami.Units.smallSpacing * 2) + - (closeButton.visible ? closeButton.width + Kirigami.Units.smallSpacing : 0) + readonly property bool multiline: remainingWidth <= 0 || actionsLayout.atBottom + + Kirigami.Icon { + id: icon + + width: Kirigami.Units.iconSizes.smallMedium + height: actionsLayout.atBottom ? width : width + + anchors { + left: parent.left + top: actionsLayout.atBottom ? parent.top : undefined + verticalCenter: actionsLayout.atBottom ? undefined : parent.verticalCenter + } + + source: { + if (root.icon.name) { + return root.icon.name; + } else if (root.icon.source) { + return root.icon.source; + } + + if (root.type === Kirigami.MessageType.Positive) { + return "dialog-positive"; + } else if (root.type === Kirigami.MessageType.Warning) { + return "dialog-warning"; + } else if (root.type === Kirigami.MessageType.Error) { + return "dialog-error"; + } + + return "dialog-information"; + } + + color: root.icon.color + } + + MouseArea { + id: textArea + + anchors { + left: icon.right + leftMargin: Kirigami.Units.smallSpacing + right: closeButton.visible ? closeButton.left : parent.right + rightMargin: closeButton.visible ? Kirigami.Units.smallSpacing : 0 + top: parent.top + bottom: contentLayout.multiline ? undefined : parent.bottom + } + + cursorShape: text.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + + implicitWidth: text.implicitWidth + height: contentLayout.multiline ? text.implicitHeight : implicitHeight + + Controls.Label { + id: text + + width: parent.width + height: parent.height + + color: Kirigami.Theme.textColor + wrapMode: Text.WordWrap + elide: Text.ElideRight + + text: root.text + + verticalAlignment: Text.AlignVCenter + + onLinkHovered: root.linkHovered(link) + onLinkActivated: root.linkActivated(link) + } + } + + Kirigami.ActionToolBar { + id: actionsLayout + + flat: false + actions: root.actions + visible: root.actions.length + alignment: Qt.AlignRight + + readonly property bool atBottom: (root.actions.length > 0) && (text.lineCount > 1 || implicitWidth > contentLayout.remainingWidth) + + anchors { + left: parent.left + top: atBottom ? textArea.bottom : parent.top + topMargin: atBottom ? Kirigami.Units.gridUnit : 0 + right: (!atBottom && closeButton.visible) ? closeButton.left : parent.right + rightMargin: !atBottom && closeButton.visible ? Kirigami.Units.smallSpacing : 0 + } + } + + Controls.ToolButton { + id: closeButton + + visible: root.showCloseButton + + anchors { + right: parent.right + top: actionsLayout.atBottom ? parent.top : undefined + verticalCenter: actionsLayout.atBottom ? undefined : parent.verticalCenter + } + + height: actionsLayout.atBottom ? implicitHeight : implicitHeight + + icon.name: "dialog-close" + + onClicked: root.visible = false + } + + Component.onCompleted: complete = true + } +} diff --git a/src/controls/templates/OverlayDrawer.qml b/src/controls/templates/OverlayDrawer.qml new file mode 100644 index 0000000..c728e4b --- /dev/null +++ b/src/controls/templates/OverlayDrawer.qml @@ -0,0 +1,524 @@ +/* + * SPDX-FileCopyrightText: 2012 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Templates 2.2 as T2 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.11 +import "private" + +/** + * 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 + */ +T2.Drawer { + id: root + + z: modal ? (Math.round((position * 10000000)) ): 100 + +//BEGIN properties + /** + * @brief This property tells whether the drawer is open and visible. + * + * default: ``false`` + */ + property bool drawerOpen: false + + /** + * @brief This property sets whether the drawer receives mouse and keyboard events. + * + * default: ``true`` + * + * @see QtQuick.Item::enabled + */ + property bool enabled: true + + /** + * @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: 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 IconPropertiesGroup handleOpenIcon: 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 IconPropertiesGroup handleClosedIcon: IconPropertiesGroup { + id: handleClosedIconGroup + 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: typeof(applicationWindow)===typeof(Function) && applicationWindow() ? applicationWindow().controlsVisible : 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: MouseArea { + id: drawerHandle + z: root.modal ? applicationWindow().overlay.z + (root.position > 0 ? +1 : -1) : root.background.parent.z + 1 + preventStealing: true + hoverEnabled: handleAnchor && handleAnchor.visible + parent: applicationWindow().overlay.parent + + QQC2.ToolButton { + anchors.centerIn: parent + width: parent.height - Units.smallSpacing * 1.5 + height: parent.height - Units.smallSpacing * 1.5 + onClicked: root.drawerOpen = !root.drawerOpen; + Accessible.name: root.drawerOpen ? root.handleOpenToolTip : root.handleClosedToolTip + visible: !Settings.tabletMode && !Settings.hasTransientTouchInput + } + + T2.ToolTip.visible: containsMouse + T2.ToolTip.text: root.drawerOpen ? handleOpenToolTip : handleClosedToolTip + T2.ToolTip.delay: Units.toolTipDelay + + property Item handleAnchor: (applicationWindow().pageStack && applicationWindow().pageStack.globalToolBar) + ? ((root.edge === Qt.LeftEdge && Qt.application.layoutDirection === Qt.LeftToRight) + || (root.edge === Qt.RightEdge && Qt.application.layoutDirection === Qt.RightToLeft) + ? applicationWindow().pageStack.globalToolBar.leftHandleAnchor + : applicationWindow().pageStack.globalToolBar.rightHandleAnchor) + : (applicationWindow().header && applicationWindow().header.toString().indexOf("ToolBarApplicationHeader") !== -1 ? applicationWindow().header : null) + + property int startX + property int mappedStartX + + enabled: root.handleVisible + + onPressed: { + root.peeking = true; + startX = mouse.x; + mappedStartX = mapToItem(parent, startX, 0).x + } + onPositionChanged: { + if (!pressed) { + return; + } + var pos = mapToItem(parent, mouse.x - startX, mouse.y); + switch(root.edge) { + case Qt.LeftEdge: + root.position = pos.x/root.contentItem.width; + break; + case Qt.RightEdge: + root.position = (root.parent.width - pos.x - width)/root.contentItem.width; + break; + default: + } + } + onReleased: { + root.peeking = false; + if (Math.abs(mapToItem(parent, mouse.x, 0).x - mappedStartX) < Qt.styleHints.startDragDistance) { + if (!root.drawerOpen) { + root.close(); + } + root.drawerOpen = !root.drawerOpen; + } + } + onCanceled: { + root.peeking = false + } + x: { + switch(root.edge) { + case Qt.LeftEdge: + return root.background.width * root.position + Units.smallSpacing; + case Qt.RightEdge: + return drawerHandle.parent.width - (root.background.width * root.position) - width - Units.smallSpacing; + default: + return 0; + } + } + y: handleAnchor && anchors.bottom ? handleAnchor.ScenePosition.y : 0 + + anchors { + bottom: drawerHandle.handleAnchor && drawerHandle.handleAnchor.visible ? undefined : parent.bottom + bottomMargin: { + if (typeof applicationWindow === "undefined") { + return; + } + + var margin = Units.smallSpacing; + if (applicationWindow().footer) { + margin = applicationWindow().footer.height + Units.smallSpacing; + } + + if(root.parent && root.height < root.parent.height) { + margin = root.parent.height - root.height - root.y + Units.smallSpacing; + } + + if (!applicationWindow() || !applicationWindow().pageStack || + !applicationWindow().pageStack.contentItem || + !applicationWindow().pageStack.contentItem.itemAt) { + return margin; + } + + var item; + if (applicationWindow().pageStack.layers.depth > 1) { + item = applicationWindow().pageStack.layers.currentItem; + } else { + item = applicationWindow().pageStack.contentItem.itemAt(applicationWindow().pageStack.contentItem.contentX + drawerHandle.x, 0); + } + + // try to take the last item + if (!item) { + item = applicationWindow().pageStack.lastItem; + } + + var pageFooter = item && item.page ? item.page.footer : (item ? item.footer : undefined); + if (pageFooter && root.parent) { + margin = root.height < root.parent.height ? margin : margin + pageFooter.height + } + + return margin; + } + Behavior on bottomMargin { + NumberAnimation { + duration: Units.shortDuration + easing.type: Easing.InOutQuad + } + } + } + + visible: root.enabled && (root.edge === Qt.LeftEdge || root.edge === Qt.RightEdge) && opacity > 0 + width: handleAnchor && handleAnchor.visible ? handleAnchor.width : Units.iconSizes.smallMedium + Units.smallSpacing*2 + height: handleAnchor && handleAnchor.visible ? handleAnchor.height : width + opacity: root.handleVisible ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + transform: Translate { + id: translateTransform + x: root.handleVisible ? 0 : (root.edge === Qt.LeftEdge ? -drawerHandle.width : drawerHandle.width) + Behavior on x { + NumberAnimation { + duration: Units.longDuration + easing.type: !root.handleVisible ? Easing.OutQuad : Easing.InQuad + } + } + } + } +//END properties + + interactive: modal + + Theme.inherit: false + Theme.colorSet: modal ? Theme.View : Theme.Window + Theme.onColorSetChanged: { + contentItem.Theme.colorSet = Theme.colorSet + background.Theme.colorSet = Theme.colorSet + } + +//BEGIN reassign properties + //default paddings + leftPadding: Units.smallSpacing + topPadding: Units.smallSpacing + rightPadding: Units.smallSpacing + bottomPadding: Units.smallSpacing + + y: modal ? 0 : ((T2.ApplicationWindow.menuBar ? T2.ApplicationWindow.menuBar.height : 0) + (T2.ApplicationWindow.header ? T2.ApplicationWindow.header.height : 0)) + + height: parent && (root.edge === Qt.LeftEdge || root.edge === Qt.RightEdge) ? (modal ? parent.height : (parent.height - y - (T2.ApplicationWindow.footer ? T2.ApplicationWindow.footer.height : 0))) : implicitHeight + + parent: modal || edge === Qt.LeftEdge || edge === Qt.RightEdge ? T2.ApplicationWindow.overlay : T2.ApplicationWindow.contentItem + + edge: Qt.LeftEdge + modal: true + + dragMargin: enabled && (edge === Qt.LeftEdge || edge === Qt.RightEdge) ? Math.min(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 (Settings.isMobile) { + collapsed = false; + } + if (!__internal.completed) { + return; + } + if ((!collapsible || modal) && collapsed) { + collapsed = true; + } + } + onCollapsibleChanged: { + if (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(); + } + } + + 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.Theme.colorSet = Theme.colorSet; + background.Theme.colorSet = 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)*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: 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..c318455 --- /dev/null +++ b/src/controls/templates/OverlaySheet.qml @@ -0,0 +1,829 @@ +/* + * SPDX-FileCopyrightText: 2016-2020 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + + +import QtQuick 2.12 +import QtQuick.Layouts 1.2 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.14 +import QtQuick.Templates 2.0 as T2 +import "private" +import "../private" + +/** + * @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. + * + * @since 2.0 + * @inherit QtQuick.QtObject + */ +QtObject { + id: root + + Theme.colorSet: Theme.View + Theme.inherit: false + + /** + * @brief This property holds the visual content item. + * @note The content item is automatically resized to fill the + * sheet's view area. + * + * Conversely, the Sheet will be sized based on the size hints + * of the contentItem, so if you need a custom size sheet, + * redefine contentWidth and contentHeight of your contentItem + */ + default property Item contentItem + + /** + * @brief This property tells whether the sheet is open and displaying its contents. + */ + property bool sheetOpen + + /** + * @brief This property holds the left padding. + * + * default: ``Kirigami.Units.largeSpacing`` + */ + property int leftPadding: Units.largeSpacing + + /** + * @brief This property holds the top padding. + * + * default: ``Kirigami.Units.largeSpacing`` + */ + property int topPadding: Units.largeSpacing + + /** + * @brief This property holds the right padding. + * + * default: ``Kirigami.Units.largeSpacing`` + */ + property int rightPadding: Units.largeSpacing + + /** + * @brief This property holds the bottom padding. + * + * default: ``Kirigami.Units.largeSpacing`` + */ + property int bottomPadding: Units.largeSpacing + + /** + * @brief This property holds the left inset for the background. + * + * The inset gets applied to both the content and the background. + * + * default: ``0`` + * + * @since 2.12 + */ + property real leftInset: 0 + + /** + * @brief This property holds the top inset for the background. + * + * The inset gets applied to both the content and the background. + * + * default: ``0`` + * + * @since 2.12 + */ + property real topInset: 0 + + /** + * @brief This property holds the right inset for the background. + * + * The inset gets applied to both the content and the background. + * + * default: ``0`` + * + * @since 2.12 + */ + property real rightInset: 0 + + /** + * @brief This property holds the bottom inset for the background. + * + * The inset gets applied to both the content and the background. + * + * default: ``0`` + * + * @since 2.12 + */ + property real bottomInset: 0 + + /** + * @brief This property holds an optional item which will be used as the sheet's header, + * and will always be displayed. + * @since 5.43 + */ + property Item header: Heading { + level: 1 + text: root.title + elide: Text.ElideRight + + // use tooltip for long text that is elided + QQC2.ToolTip.visible: truncated && titleHoverHandler.hovered + QQC2.ToolTip.text: root.title + HoverHandler { + id: titleHoverHandler + } + } + + /** + * @brief An optional item which will be used as the sheet's footer, + * always kept on screen. + * @since 5.43 + */ + property Item footer + + /** + * @brief This property holds the background item. + * + * @note If the background item has no explicit size specified, + * it automatically follows the control's size. In most cases, + * there is no need to specify width or height for a background item. + */ + property Item background + + /** + * @brief This property sets the visibility of the close button in the top-right corner. + * + * default: `Only shown in desktop mode` + * + * @since 5.44 + */ + property bool showCloseButton: !Settings.isMobile + + /** + * @brief This property holds the sheet's title. + * @note If the header property is set, this will have no effect as the heading will be replaced by the header. + * @since 5.84 + */ + property string title + + property Item parent + + /** + * @brief This function opens the overlay sheet. + */ + function open() { + openAnimation.running = true; + root.sheetOpen = true; + contentLayout.initialHeight = contentLayout.height + mainItem.visible = true; + mainItem.forceActiveFocus(); + } + + /** + * @brief This function closes the overlay sheet. + */ + function close() { + if (root.sheetOpen) { + root.sheetOpen = false; + } + } + + onBackgroundChanged: { + background.parent = contentLayout.parent; + background.anchors.fill = contentLayout; + background.anchors.margins = -1 + background.z = -1; + } + onContentItemChanged: { + if (contentItem instanceof Flickable) { + scrollView.flickableItem = contentItem; + contentItem.parent = scrollView; + scrollView.contentItem = contentItem; + scrollView.viewContent = contentItem.contentItem; + } else { + contentItem.parent = contentItemParent; + flickableContents.parent = scrollView.flickableItem.contentItem; + flickableContents.anchors.top = scrollView.flickableItem.contentItem.top; + flickableContents.anchors.left = scrollView.flickableItem.contentItem.left; + flickableContents.anchors.right = scrollView.flickableItem.contentItem.right; + scrollView.viewContent = flickableContents; + contentItem.anchors.left = contentItemParent.left; + contentItem.anchors.right = contentItemParent.right; + } + scrollView.flickableItem.interactive = false; + scrollView.flickableItem.flickableDirection = Flickable.VerticalFlick; + } + onSheetOpenChanged: { + if (sheetOpen) { + open(); + } else { + closeAnimation.restart() + Qt.inputMethod.hide(); + root.parent.forceActiveFocus(); + } + } + onHeaderChanged: headerItem.initHeader() + onFooterChanged: { + footer.parent = footerParent; + footer.anchors.fill = footerParent; + } + + Component.onCompleted: { + // ScrollablePage must do things related to parenting of OverlaySheets in its conCompleted, so this must execute later + // TODO KF6: port the root object to Popup template? + Qt.callLater(() => { + if (!root.parent && typeof applicationWindow !== "undefined") { + root.parent = applicationWindow().overlay + } + headerItem.initHeader(); + }); + } + + readonly property Item rootItem: FocusScope { + id: mainItem + Theme.colorSet: root.Theme.colorSet + Theme.inherit: root.Theme.inherit + z: 101 + // we want to be over any possible OverlayDrawers, including handles + parent: { + if (root.parent && root.parent.ColumnView.view && (root.parent.ColumnView.view === root.parent || root.parent.ColumnView.view === root.parent.parent)) { + return root.parent.ColumnView.view.parent; + } else if (root.parent && root.parent.overlay) { + root.parent.overlay; + } else { + return root.parent; + } + } + + anchors.fill: parent + + visible: false + clip: true + + // differentiate between mouse and touch + HoverHandler { + id: mouseHover + acceptedDevices: PointerDevice.Mouse + } + + Keys.onEscapePressed: { + if (root.sheetOpen) { + root.close(); + } else { + event.accepted = false; + } + } + + readonly property int contentItemPreferredWidth: root.contentItem.Layout.preferredWidth > 0 ? root.contentItem.Layout.preferredWidth : root.contentItem.implicitWidth + + readonly property int absoluteContentItemMaximumWidth: width <= 0 ? contentItemPreferredWidth : Math.round(width - Units.largeSpacing * 2) + readonly property int contentItemMaximumWidth: root.contentItem.Layout.maximumWidth > 0 ? Math.min(root.contentItem.Layout.maximumWidth, absoluteContentItemMaximumWidth) : width > Units.gridUnit * 30 ? width * 0.95 : absoluteContentItemMaximumWidth + + onHeightChanged: { + var focusItem; + + focusItem = Window.activeFocusItem; + + if (!focusItem) { + return; + } + + // NOTE: there is no function to know if an item is descended from another, + // so we have to walk the parent hierarchy by hand + var isDescendent = false; + var candidate = focusItem.parent; + while (candidate) { + if (candidate === root) { + isDescendent = true; + break; + } + candidate = candidate.parent; + } + if (!isDescendent) { + return; + } + + var cursorY = 0; + if (focusItem.cursorPosition !== undefined) { + cursorY = focusItem.positionToRectangle(focusItem.cursorPosition).y; + } + + + var pos = focusItem.mapToItem(flickableContents, 0, cursorY - Units.gridUnit*3); + // focused item already visible? add some margin for the space of the action buttons + if (pos.y >= scrollView.flickableItem.contentY && pos.y <= scrollView.flickableItem.contentY + scrollView.flickableItem.height - Units.gridUnit * 8) { + return; + } + scrollView.flickableItem.contentY = pos.y; + } + + ParallelAnimation { + id: openAnimation + property int margins: Units.gridUnit * 5 + NumberAnimation { + target: outerFlickable + properties: "contentY" + from: -outerFlickable.height + to: outerFlickable.openPosition + duration: Units.longDuration + easing.type: Easing.OutQuad + } + OpacityAnimator { + target: mainItem + from: 0 + to: 1 + duration: Units.longDuration + easing.type: Easing.InQuad + } + } + + NumberAnimation { + id: resetAnimation + target: outerFlickable + properties: "contentY" + from: outerFlickable.contentY + to: outerFlickable.visibleArea.yPosition < (1 - outerFlickable.visibleArea.heightRatio)/2 || scrollView.flickableItem.contentHeight < outerFlickable.height + ? outerFlickable.openPosition + : outerFlickable.contentHeight - outerFlickable.height + outerFlickable.topEmptyArea + headerItem.height + footerItem.height + duration: Units.longDuration + easing.type: Easing.OutQuad + } + + SequentialAnimation { + id: closeAnimation + ParallelAnimation { + NumberAnimation { + target: outerFlickable + properties: "contentY" + from: outerFlickable.contentY + (contentLayout.initialHeight - contentLayout.height) + to: outerFlickable.visibleArea.yPosition < (1 - outerFlickable.visibleArea.heightRatio)/2 ? -mainItem.height : outerFlickable.contentHeight + duration: Units.longDuration + easing.type: Easing.InQuad + } + OpacityAnimator { + target: mainItem + from: 1 + to: 0 + duration: Units.longDuration + easing.type: Easing.InQuad + } + } + ScriptAction { + script: { + contentLayout.initialHeight = 0 + scrollView.flickableItem.contentY = -mainItem.height; + mainItem.visible = false; + } + } + } + Rectangle { + anchors.fill: parent + color: "black" + opacity: 0.3 * Math.min( + (Math.min(outerFlickable.contentY + outerFlickable.height, outerFlickable.height) / outerFlickable.height), + (2 + (outerFlickable.contentHeight - outerFlickable.contentY - outerFlickable.topMargin - outerFlickable.bottomMargin)/outerFlickable.height)) + } + + MouseArea { + anchors.fill: parent + drag.filterChildren: true + hoverEnabled: true + + onPressed: { + let pos = mapToItem(contentLayout, mouse.x, mouse.y); + if (contentLayout.contains(pos) && mouseHover.hovered) { // only on mouse event, not touch + // disable dragging the sheet with a mouse + outerFlickable.interactive = false + } + } + onReleased: { + let pos = mapToItem(contentLayout, mouse.x, mouse.y); + if (!contentLayout.contains(pos)) { + root.close(); + } + // enable dragging of sheet once mouse is not clicked + outerFlickable.interactive = true + } + + + Item { + id: flickableContents + + readonly property real listHeaderHeight: scrollView.flickableItem ? -scrollView.flickableItem.originY : 0 + + y: (scrollView.contentItem !== flickableContents ? -scrollView.flickableItem.contentY - listHeaderHeight - (headerItem.visible ? headerItem.height : 0): 0) + + width: mainItem.contentItemPreferredWidth <= 0 ? mainItem.width : (mainItem.contentItemMaximumWidth > 0 ? Math.min( mainItem.contentItemMaximumWidth, Math.max( mainItem.width/2, mainItem.contentItemPreferredWidth ) ) : Math.max( mainItem.width / 2, mainItem.contentItemPreferredWidth ) ) + leftPadding + rightPadding + + + implicitHeight: scrollView.viewContent === flickableContents ? root.contentItem.height + topPadding + bottomPadding : 0 + + Connections { + target: enabled ? flickableContents.Window.activeFocusItem : null + enabled: flickableContents.focus && flickableContents.Window.activeFocusItem && flickableContents.Window.activeFocusItem.hasOwnProperty("text") + function onTextChanged() { + if (Qt.inputMethod.cursorRectangle.y + Qt.inputMethod.cursorRectangle.height > mainItem.Window.height) { + scrollView.flickableItem.contentY += (Qt.inputMethod.cursorRectangle.y + Qt.inputMethod.cursorRectangle.height) - mainItem.Window.height + } + } + } + + Item { + id: contentItemParent + anchors { + fill: parent + leftMargin: leftPadding + topMargin: topPadding + rightMargin: rightPadding + bottomMargin: bottomPadding + } + } + } + + Connections { + target: scrollView.flickableItem + property real oldContentHeight: 0 + function onContentHeightChanged() { + if (openAnimation.running) { + openAnimation.running = false; + open(); + } else { + // repositioning is relevant only when the content height is less than the viewport height. + // In that case the sheet looks like a dialog and should be centered. there is also a corner case when now is bigger then the viewport but prior to the + // resize event it was smaller, also in this case we need repositioning + if (scrollView.animatedContentHeight < outerFlickable.height + || scrollView.flickableItem.oldContentHeight < outerFlickable.height + ) { + outerFlickable.adjustPosition(); + } + oldContentHeight = scrollView.animatedContentHeight + } + } + } + + Flickable { + id: outerFlickable + anchors.fill: parent + contentWidth: width + topMargin: height + bottomMargin: height + // +1: we need the flickable to be always interactive + contentHeight: Math.max(height+1, scrollView.animatedContentHeight + topEmptyArea) + + // readonly property int topEmptyArea: Math.max(height-scrollView.animatedContentHeight, Units.gridUnit * 3) + readonly property int topEmptyArea: Math.max(height-scrollView.animatedContentHeight, Units.gridUnit * 3) + + readonly property real openPosition: Math.max(0, outerFlickable.height - outerFlickable.contentHeight + headerItem.height + footerItem.height) + height/2 - contentLayout.height/2; + + onOpenPositionChanged: { + if (openAnimation.running) { + openAnimation.running = false; + root.open(); + } else if (root.sheetOpen) { + adjustPosition(); + } + } + + property int oldContentY: NaN + property int oldContentHeight: 0 + property bool lastMovementWasDown: false + property real startDraggingPos + property bool layoutMovingGuard: false + WheelHandler { + target: outerFlickable + scrollFlickableTarget: false + } + + function adjustPosition() { + if(layoutMovingGuard) return; + + if (openAnimation.running) { + openAnimation.running = false; + open() + } else { + resetAnimation.running = false; + contentY = openPosition; + } + } + + // disable dragging the sheet with a mouse on header bar + MouseArea { + anchors.fill: parent + onPressed: { + if (mouseHover.hovered) { // only on mouse event, not touch + outerFlickable.interactive = false + } + } + onReleased: outerFlickable.interactive = true + } + + onContentYChanged: { + if (scrollView.userInteracting) { + return; + } + + let startPos = -scrollView.flickableItem.topMargin - flickableContents.listHeaderHeight; + let pos = contentY - topEmptyArea - flickableContents.listHeaderHeight; + let endPos = scrollView.animatedContentHeight - scrollView.flickableItem.height + scrollView.flickableItem.bottomMargin - flickableContents.listHeaderHeight; + + layoutMovingGuard = true; + if (endPos - pos > 0) { + contentLayout.y = Math.round(Math.max(root.topInset, scrollView.flickableItem.topMargin - pos - flickableContents.listHeaderHeight)); + } else if (scrollView.flickableItem.topMargin - pos < 0) { + contentLayout.y = Math.round(endPos - pos + root.topInset); + } + layoutMovingGuard = false; + + scrollView.flickableItem.contentY = Math.max( + startPos, Math.min(pos, endPos)); + + lastMovementWasDown = contentY < oldContentY; + oldContentY = contentY; + } + + onFlickEnded: { + if (openAnimation.running || closeAnimation.running) { + return; + } + if (scrollView.flickableItem.atYBeginning ||scrollView.flickableItem.atYEnd) { + resetAnimation.restart(); + } + } + + onDraggingChanged: { + if (dragging) { + startDraggingPos = contentY; + return; + } + + let shouldClose = false; + + // close + if (scrollView.flickableItem.atYBeginning) { + if (startDraggingPos - contentY > Units.gridUnit * 4 && + contentY < -Units.gridUnit * 4 && + lastMovementWasDown) { + shouldClose = true; + } + } + + if (scrollView.flickableItem.atYEnd) { + if (contentY - startDraggingPos > Units.gridUnit * 4 && + contentY > contentHeight - height + Units.gridUnit * 4 && + !lastMovementWasDown) { + shouldClose = true; + } + } + + if (shouldClose) { + root.sheetOpen = false + } else if (scrollView.flickableItem.atYBeginning || scrollView.flickableItem.atYEnd) { + resetAnimation.restart(); + } + } + + onHeightChanged: { + adjustPosition(); + } + + onContentHeightChanged: { + // repositioning is relevant only when the content height is less than the viewport height. + // In that case the sheet looks like a dialog and should be centered. there is also a corner case when now is bigger then the viewport but prior to the + // resize event it was smaller, also in this case we need repositioning + if (contentHeight < height || oldContentHeight < height) { + adjustPosition(); + } + oldContentHeight = contentHeight; + } + + ColumnLayout { + id: contentLayout + spacing: 0 + // Its events should be filtered but not scrolled + parent: outerFlickable + anchors.horizontalCenter: parent.horizontalCenter + width: mainItem.contentItemPreferredWidth <= 0 ? mainItem.width : (mainItem.contentItemMaximumWidth > 0 ? Math.min( mainItem.contentItemMaximumWidth, Math.max( mainItem.width/2, mainItem.contentItemPreferredWidth ) ) : Math.max( mainItem.width / 2, mainItem.contentItemPreferredWidth ) ) - root.leftInset - root.rightInset + root.leftPadding + root.rightPadding + height: Math.min(implicitHeight, parent.height) - root.topInset - root.bottomInset + property real initialHeight + + Behavior on height { + NumberAnimation { + duration: Units.shortDuration + easing.type: Easing.InOutCubic + } + } + + // 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 + ShadowedRectangle { + id: headerItem + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + //Layout.margins: 1 + visible: root.header || root.showCloseButton + implicitHeight: Math.max(headerParent.implicitHeight, closeIcon.height) + Units.smallSpacing * 2 + z: 2 + corners.topLeftRadius: Units.smallSpacing + corners.topRightRadius: Units.smallSpacing + Theme.colorSet: Theme.Header + Theme.inherit: false + color: Theme.backgroundColor + + function initHeader() { + if (header) { + header.parent = headerParent; + header.anchors.fill = headerParent; + + // TODO: special case for actual ListViews + } + } + + Item { + id: headerParent + implicitHeight: header ? header.implicitHeight : 0 + anchors { + fill: parent + leftMargin: Units.largeSpacing + margins: Units.smallSpacing + rightMargin: (root.showCloseButton ? closeIcon.width : 0) + Units.smallSpacing + } + } + Icon { + id: closeIcon + + readonly property bool tallHeader: headerItem.height > (Units.iconSizes.smallMedium + Units.largeSpacing + Units.largeSpacing) + + anchors { + right: parent.right + rightMargin: Units.largeSpacing + verticalCenter: headerItem.verticalCenter + margins: Units.smallSpacing + } + + // Apply the changes to the anchors imperatively, to first disable an anchor point + // before setting the new one, so the icon don't grow unexpectedly + onTallHeaderChanged: { + if (tallHeader) { + // We want to position the close button in the top-right corner if the header is very tall + anchors.verticalCenter = undefined + anchors.topMargin = Units.largeSpacing + anchors.top = headerItem.top + } else { + // but we want to vertically center it in a short header + anchors.top = undefined + anchors.topMargin = undefined + anchors.verticalCenter = headerItem.verticalCenter + } + } + Component.onCompleted: tallHeaderChanged() + + z: 3 + visible: root.showCloseButton + width: Units.iconSizes.smallMedium + height: width + source: closeMouseArea.containsMouse ? "window-close" : "window-close-symbolic" + active: closeMouseArea.containsMouse + MouseArea { + id: closeMouseArea + hoverEnabled: true + anchors.fill: parent + onClicked: root.close(); + } + } + Separator { + anchors { + right: parent.right + left: parent.left + top: parent.bottom + } + } + } + + QQC2.ScrollView { + id: scrollView + + // Don't do the automatic interactive enable/disable + // canFlickWithMouse: true + property Item viewContent + property real animatedContentHeight: flickableItem.contentHeight + property bool userInteracting: false + Layout.fillWidth: true + Layout.fillHeight: true + property alias flickableItem: scrollView.contentItem + + focus: false + + implicitHeight: flickableItem.contentHeight + Layout.maximumHeight: flickableItem.contentHeight + + Layout.alignment: Qt.AlignTop + + // HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890) + QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff + + Behavior on animatedContentHeight { + NumberAnimation { + duration: Units.shortDuration + easing.type: Easing.InOutCubic + } + } + } + + Item { Layout.fillHeight: true } + + Connections { + target: scrollView.flickableItem + property real oldContentY: 0 + function onContentYChanged() { + if (outerFlickable.moving) { + oldContentY = scrollView.flickableItem.contentY; + return; + } + scrollView.userInteracting = true; + + let diff = scrollView.flickableItem.contentY - oldContentY + + outerFlickable.contentY = outerFlickable.contentY + diff; + + if (diff > 0) { + contentLayout.y = Math.max(root.topInset, contentLayout.y - diff); + } else if (scrollView.flickableItem.contentY < outerFlickable.topEmptyArea + headerItem.height) { + contentLayout.y = Math.min(outerFlickable.topEmptyArea, contentLayout.y + (contentLayout.y - diff)) + root.topInset; + } + + oldContentY = scrollView.flickableItem.contentY; + scrollView.userInteracting = false; + } + } + Item { + visible: footerItem.visible + implicitHeight: footerItem.height + } + } + + // footer item is outside the layout as it should never scroll away + + // 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 + ShadowedRectangle { + id: footerItem + width: contentLayout.width - 2 + corners.bottomLeftRadius: Units.smallSpacing + corners.bottomRightRadius: Units.smallSpacing + parent: outerFlickable + x: contentLayout.x + 1 + y: Math.min(parent.height, contentLayout.y + contentLayout.height -1) - height + visible: root.footer + implicitHeight: footerParent.implicitHeight + Units.smallSpacing * 2 + extraMargin + Theme.colorSet: Theme.Window + Theme.inherit: false + color: Theme.backgroundColor + + // Show an extra margin when: + // * the application is in mobile mode + // * it doesn't use toolbarapplicationheader + // * the bottom screen controls are visible + // * the sheet is displayed *under* the controls + property int extraMargin: (!root.parent || + !Settings.isMobile || + typeof applicationWindow === "undefined" || + (root.parent === applicationWindow().overlay) || + !applicationWindow().controlsVisible || + (applicationWindow().pageStack && applicationWindow().pageStack.globalToolBar && applicationWindow().pageStack.globalToolBar.actualStyle === ApplicationHeaderStyle.ToolBar) || + (applicationWindow().header && applicationWindow().header.toString().indexOf("ToolBarApplicationHeader") === 0)) + ? 0 : Units.gridUnit * 3 + + z: 2 + Item { + id: footerParent + implicitHeight: footer ? footer.implicitHeight : 0 + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: Units.smallSpacing + } + } + + Separator { + anchors { + right: parent.right + left: parent.left + bottom: parent.top + leftMargin: -1 + rightMargin: -1 + } + } + } + } + } + } +} diff --git a/src/controls/templates/SingletonHeaderSizeGroup.qml b/src/controls/templates/SingletonHeaderSizeGroup.qml new file mode 100644 index 0000000..7e90569 --- /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 2.14 as Kirigami + +Kirigami.SizeGroup { + mode: Kirigami.SizeGroup.Height +} \ No newline at end of file diff --git a/src/controls/templates/SwipeListItem.qml b/src/controls/templates/SwipeListItem.qml new file mode 100644 index 0000000..a0ea356 --- /dev/null +++ b/src/controls/templates/SwipeListItem.qml @@ -0,0 +1,551 @@ +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.6 +import QtQuick.Layouts 1.4 +import QtQuick.Controls 2.4 as Controls +import QtQuick.Templates 2.4 as T +import org.kde.kirigami 2.11 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. + * Any subclass of AbstractListItem can be assigned as the contentItem property. + * + * 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 + */ +T.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 + */ + 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: ``true`` + */ + property bool separatorVisible: true + + /** + * @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 an AbstractListItem, + * 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 an AbstractListItem, + * 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 + + // TODO KF6 remove this super wrong thing + /// @private + /// @deprecated This property will be removed in KDE Framework 6. Use contentItem instead. + default property alias _default: listItem.contentItem +//END properties + + LayoutMirroring.childrenInherit: true + + hoverEnabled: true + implicitWidth: contentItem ? contentItem.implicitWidth : Kirigami.Units.gridUnit * 12 + width: parent ? parent.width : implicitWidth + implicitHeight: Math.max(Kirigami.Units.gridUnit * 2, contentItem.implicitHeight) + 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 + + contentItem: Item {} + QtObject { + id: internal + + property Flickable view: listItem.ListView.view || (listItem.parent ? (listItem.parent.ListView.view || (listItem.parent instanceof Flickable ? listItem.parent : null)) : null) + + readonly property QtObject swipeFilterItem: (view && view.parent && view.parent.parent && view.parent.parent._swipeFilter) ? view.parent.parent._swipeFilter : null + + readonly property bool edgeEnabled: swipeFilterItem ? swipeFilterItem.currentItem === listItem || swipeFilterItem.currentItem === listItem.parent : false + + property bool indicateActiveFocus: listItem.pressed || Kirigami.Settings.tabletMode || listItem.activeFocus || (view ? view.activeFocus : false) + + // install the SwipeItemEventFilter + onViewChanged: { + if (listItem.alwaysVisibleActions || !Kirigami.Settings.tabletMode) { + return; + } + if (internal.view && Kirigami.Settings.tabletMode && !internal.view.parent.parent._swipeFilter) { + var component = Qt.createComponent(Qt.resolvedUrl("../private/SwipeItemEventFilter.qml")); + internal.view.parent.parent._swipeFilter = component.createObject(internal.view.parent.parent); + } + } + } + + Connections { + target: Kirigami.Settings + function onTabletModeChanged() { + if (Kirigami.Settings.tabletMode) { + if (!internal.swipeFilterItem) { + var component = Qt.createComponent(Qt.resolvedUrl("../private/SwipeItemEventFilter.qml")); + listItem.ListView.view.parent.parent._swipeFilter = component.createObject(listItem.ListView.view.parent.parent); + } + } 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 int expectedLeftPadding: listItem.padding * 2 + (listItem.mirrored ? overlayLoader.paddingOffset : 0) + readonly property int expectedRightPadding: listItem.padding * 2 + (listItem.mirrored ? 0 : overlayLoader.paddingOffset) + readonly property var theAlias: anchors + function validate(want, defaultValue) { + 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 != this.expectedLeftPadding || listItem.rightPadding != this.expectedRightPadding) { + listItem.leftPadding = Qt.binding(() => this.expectedLeftPadding) + listItem.rightPadding = Qt.binding(() => this.expectedRightPadding) + console.warn(warningText) + return defaultValue + } + + return want + } + anchors { + right: validate((Qt.application.layoutDirection === Qt.RightToLeft) ? undefined : (contentItem ? contentItem.right : undefined), contentItem ? contentItem.right : undefined) + rightMargin: validate(-paddingOffset, 0) + left: validate((Qt.application.layoutDirection === Qt.LeftToRight) ? 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 || !listItem.supportsMouseEvents ? 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: startX = mapToItem(listItem, 0, 0).x; + onClicked: { + if (Math.abs(mapToItem(listItem, 0, 0).x - startX) > Qt.styleHints.startDragDistance) { + return; + } + if (listItem.LayoutMirroring.enabled) { + 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: { + var pos = mapToItem(listItem, mouse.x, mouse.y); + + if (listItem.LayoutMirroring.enabled) { + 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: { + if (listItem.LayoutMirroring.enabled) { + 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.pressed && !listItem.checked && !listItem.sectionDelegate) + source: (LayoutMirroring.enabled ? (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.LayoutMirroring.enabled) { + 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.onPressed(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 + MouseArea { + + anchors.fill: parent + + // Controls.SwipeDelegate.onPressedChanged is broken with touch + onClicked: { + slideAnim.to = 0; + slideAnim.restart(); + } + Rectangle { + anchors.fill: parent + color: parent.pressed ? Qt.darker(Kirigami.Theme.backgroundColor, 1.1) : Qt.darker(Kirigami.Theme.backgroundColor, 1.05) + } + + visible: listItem.swipe.position != 0 + + + EdgeShadow { + edge: Qt.TopEdge + visible: background.x != 0 + anchors { + right: parent.right + left: parent.left + top: parent.top + } + } + EdgeShadow { + edge: LayoutMirroring.enabled ? Qt.RightEdge : Qt.LeftEdge + x: LayoutMirroring.enabled ? listItem.background.x - width : (listItem.background.x + listItem.background.width) + visible: background.x != 0 + anchors { + top: parent.top + bottom: parent.bottom + } + } + } + } + + + RowLayout { + id: actionsLayout + 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 || listItem.swipe.rightItem || listItem + : overlayLoader + + property bool hasVisibleActions: false + function updateVisibleActions(definitelyVisible) { + if (definitelyVisible === undefined) { + definitelyVisible = false + } + + if (definitelyVisible) { + hasVisibleActions = true; + } else { + var actionCount = listItem.actions.length; + for (var i = 0; i < actionCount; i++) { + // Assuming that visible is only false if it is explicitly false, and not just falsy + if (listItem.actions[i].visible === false) { + continue; + } + hasVisibleActions = true; + break; + } + } + } + + Repeater { + model: { + if (listItem.actions.length === 0) { + return null; + } else { + return listItem.actions[0].text !== undefined && + listItem.actions[0].trigger !== undefined ? + listItem.actions : + listItem.actions[0]; + } + } + delegate: Controls.ToolButton { + icon.name: modelData.iconName !== "" ? modelData.iconName : "" + icon.source: modelData.iconSource !== "" ? modelData.iconSource : "" + enabled: (modelData && modelData.enabled !== undefined) ? modelData.enabled : true; + visible: (modelData && modelData.visible !== undefined) ? modelData.visible : true; + onVisibleChanged: actionsLayout.updateVisibleActions(visible); + Component.onCompleted: actionsLayout.updateVisibleActions(visible); + Component.onDestruction: actionsLayout.updateVisibleActions(visible); + Controls.ToolTip.delay: Kirigami.Units.toolTipDelay + Controls.ToolTip.timeout: 5000 + Controls.ToolTip.visible: listItem.visible && (Kirigami.Settings.tabletMode ? pressed : hovered) && Controls.ToolTip.text.length > 0 + Controls.ToolTip.text: modelData.tooltip || modelData.text + + onClicked: { + if (modelData && modelData.trigger !== undefined) { + modelData.trigger(); + } + slideAnim.to = 0; + slideAnim.restart(); + } + } + } + } + + + background: DefaultListItemBackground {} + + swipe { + enabled: false + right: listItem.alwaysVisibleActions ||listItem.LayoutMirroring.enabled || !Kirigami.Settings.tabletMode ? null : actionsBackgroundDelegate + left: listItem.alwaysVisibleActions ||listItem.LayoutMirroring.enabled && 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 +} + diff --git a/src/controls/templates/private/BackButton.qml b/src/controls/templates/private/BackButton.qml new file mode 100644 index 0000000..d341cde --- /dev/null +++ b/src/controls/templates/private/BackButton.qml @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.0 as Controls + +import org.kde.kirigami 2.4 + +Controls.ToolButton { + id: button + + icon.name: (LayoutMirroring.enabled ? "go-previous-symbolic-rtl" : "go-previous-symbolic") + + enabled: applicationWindow().pageStack.layers.depth > 1 || (applicationWindow().pageStack.depth > 1 && (applicationWindow().pageStack.currentIndex > 0 || applicationWindow().pageStack.contentItem.contentX > 0)) + + property var showNavButtons: { + try { + return globalToolBar.showNavigationButtons + } catch (_) { + 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: applicationWindow().pageStack.layers.depth > 1 || (applicationWindow().pageStack.contentItem.contentWidth > applicationWindow().pageStack.width + Units.gridUnit && (button.showNavButtons === true || (button.showNavButtons & ApplicationHeaderStyle.ShowBackButton))) + + onClicked: { + applicationWindow().pageStack.goBack(); + } + + Controls.ToolTip { + visible: button.hovered + text: qsTr("Navigate Back") + delay: 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..571c702 --- /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 2.5 + +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..4da9a60 --- /dev/null +++ b/src/controls/templates/private/ContextIcon.qml @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.4 + +Item { + id: canvas + width: height + height: Units.iconSizes.smallMedium + property OverlayDrawer drawer + property color color: Theme.textColor + opacity: 0.8 + layer.enabled: true + + LayoutMirroring.enabled: false + LayoutMirroring.childrenInherit: true + + Item { + id: iconRoot + anchors { + fill: parent + margins: Units.smallSpacing + } + property int thickness: 2 + Rectangle { + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + //horizontalCenterOffset: -parent.width/2 + topMargin: (parent.height/2 - iconRoot.thickness/2) * drawer.position + } + antialiasing: drawer.position !== 0 + transformOrigin: Item.Center + width: (1 - drawer.position) * height + drawer.position * (Math.sqrt(2*(parent.width*parent.width))) + height: iconRoot.thickness + color: canvas.color + rotation: 45 * drawer.position + } + + Rectangle { + anchors.centerIn: parent + width: height + height: iconRoot.thickness + color: canvas.color + } + + + Rectangle { + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + // topMargin: -iconRoot.thickness/2 * drawer.position + bottomMargin: (parent.height/2 - iconRoot.thickness/2) * drawer.position + } + antialiasing: drawer.position !== 0 + transformOrigin: Item.Center + width: (1 - drawer.position) * height + drawer.position * (Math.sqrt(2*(parent.width*parent.width))) + height: iconRoot.thickness + color: canvas.color + rotation: -45 * drawer.position + } + } +} + diff --git a/src/controls/templates/private/ForwardButton.qml b/src/controls/templates/private/ForwardButton.qml new file mode 100644 index 0000000..a33552f --- /dev/null +++ b/src/controls/templates/private/ForwardButton.qml @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.0 as Controls + +import org.kde.kirigami 2.4 + +Controls.ToolButton { + id: button + + icon.name: (LayoutMirroring.enabled ? "go-next-symbolic-rtl" : "go-next-symbolic") + + enabled: applicationWindow().pageStack.currentIndex < applicationWindow().pageStack.depth-1 + + property var showNavButtons: { + try { + return globalToolBar.showNavigationButtons + } catch (_) { + 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: applicationWindow().pageStack.layers.depth === 1 && applicationWindow().pageStack.contentItem.contentWidth > applicationWindow().pageStack.width + Units.gridUnit && (showNavButtons === true || (showNavButtons & ApplicationHeaderStyle.ShowForwardButton)) + + onClicked: applicationWindow().pageStack.goForward(); + + Controls.ToolTip { + visible: button.hovered + text: qsTr("Navigate Forward") + delay: Units.toolTipDelay + timeout: 5000 + 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..ba64b37 --- /dev/null +++ b/src/controls/templates/private/GenericDrawerIcon.qml @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.4 as Kirigami + +Item { + width: height + height: Kirigami.Units.iconSizes.smallMedium + property Kirigami.OverlayDrawer drawer + property color color: 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..0a76636 --- /dev/null +++ b/src/controls/templates/private/IconPropertiesGroup.qml @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQml 2.1 + +/** + * @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) +} + diff --git a/src/controls/templates/private/MenuIcon.qml b/src/controls/templates/private/MenuIcon.qml new file mode 100644 index 0000000..9ffb7e1 --- /dev/null +++ b/src/controls/templates/private/MenuIcon.qml @@ -0,0 +1,67 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.4 + +Item { + id: canvas + width: height + height: Units.iconSizes.smallMedium + property OverlayDrawer drawer + property color color: Theme.textColor + opacity: 0.8 + layer.enabled: true + + LayoutMirroring.enabled: false + LayoutMirroring.childrenInherit: true + Item { + id: iconRoot + anchors { + fill: parent + margins: 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/PassiveNotification.qml b/src/controls/templates/private/PassiveNotification.qml new file mode 100644 index 0000000..66b0df2 --- /dev/null +++ b/src/controls/templates/private/PassiveNotification.qml @@ -0,0 +1,214 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Controls 2.3 as Controls +import QtQuick.Layouts 1.2 +import QtQuick.Window 2.2 +import QtGraphicalEffects 1.0 +import org.kde.kirigami 2.12 as Kirigami + +/* + * PassiveNotification is a type for 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. + * This is not a full-fledged notification system. the applciation should + * use this with care and only one notification should be visible at once per app. +*/ +Controls.Popup { + id: root + + x: Math.round(parent.width/2 - width/2) + y: parent.height - height - Kirigami.Units.smallSpacing + implicitWidth: Math.max(background ? background.implicitWidth : 0, + contentWidth + leftPadding + rightPadding) + leftInset + rightInset + implicitHeight: Math.max(background ? background.implicitHeight : 0 , + contentHeight + topPadding + bottomPadding)+ topInset + bottomInset + height: implicitHeight + width: implicitWidth + + topPadding: Kirigami.Units.smallSpacing + leftPadding: Kirigami.Units.smallSpacing + rightPadding: Kirigami.Units.smallSpacing + bottomPadding: Kirigami.Units.smallSpacing + + modal: false + closePolicy: Controls.Popup.NoAutoClose + focus: false + clip: false + + 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; + } + + open(); + + for (let i = 0; i < outerLayout.children.length - 3; ++i) { + outerLayout.children[i].close(); + } + + let delegate = delegateComponent.createObject(outerLayout, { + "text": message, + "actionText": actionText || "", + "callBack": callBack || (function(){}), + "interval": interval + }); + + // Reorder items to have the last on top + let children = outerLayout.children; + for (let i in children) { + children[i].Layout.row = children.length-1-i; + } + } + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Complementary + + background: Item {} + + contentItem: GridLayout { + id: outerLayout + columns: 1 + } + + Component { + id: delegateComponent + Controls.Control { + id: delegate + property alias text: label.text + property alias actionText: actionButton.text + property alias interval: timer.interval + property var callBack + Layout.alignment: Qt.AlignCenter + Layout.bottomMargin: -delegate.height + opacity: 0 + function close() { + closeAnim.running = true; + } + + leftPadding: Kirigami.Units.largeSpacing + rightPadding: Kirigami.Units.largeSpacing + topPadding: Kirigami.Units.largeSpacing + bottomPadding: Kirigami.Units.largeSpacing + + Component.onCompleted: openAnim.restart() + ParallelAnimation { + id: openAnim + OpacityAnimator { + target: delegate + from: 0 + to: 1 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: delegate + property: "Layout.bottomMargin" + from: -delegate.height + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + + SequentialAnimation { + id: closeAnim + ParallelAnimation { + OpacityAnimator { + target: delegate + from: 1 + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: delegate + property: "Layout.bottomMargin" + to: -delegate.height + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + ScriptAction { + script: delegate.destroy(); + } + } + + contentItem: RowLayout { + id: mainLayout + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: root.Kirigami.Theme.colorSet + width: mainLayout.width + //FIXME: why this is not automatic? + implicitHeight: Math.max(label.implicitHeight, actionButton.implicitHeight) + HoverHandler { + id: hover + } + TapHandler { + acceptedButtons: Qt.LeftButton + onTapped: delegate.close(); + } + Timer { + id: timer + running: root.visible && !hover.hovered + onTriggered: delegate.close(); + } + + Controls.Label { + id: label + Layout.maximumWidth: Math.min(root.parent.width - Kirigami.Units.largeSpacing * 4, implicitWidth) + elide: Text.ElideRight + wrapMode: Text.WordWrap + maximumLineCount: 4 + } + + Controls.Button { + id: actionButton + visible: text.length > 0 + onClicked: { + delegate.close();; + if (delegate.callBack && (typeof delegate.callBack === "function")) { + delegate.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.smallSpacing * 2 + color: Kirigami.Theme.backgroundColor + opacity: 0.9 + } + } + } + + Controls.Overlay.modal: Rectangle { + color: Qt.rgba(0, 0, 0, 0.4) + } + + Controls.Overlay.modeless: Item {} +} + diff --git a/src/controls/templates/qmldir b/src/controls/templates/qmldir new file mode 100644 index 0000000..17c3c94 --- /dev/null +++ b/src/controls/templates/qmldir @@ -0,0 +1,10 @@ +module org.kde.kirigami.templates + +OverlaySheet 2.2 OverlaySheet.qml +FormLayout 2.2 FormLayout.qml +SwipeListItem 2.2 SwipeListItem.qml +AbstractListItem 2.2 AbstractListItem.qml +ApplicationHeader 2.2 ApplicationHeader.qml +AbstractApplicationHeader 2.2 AbstractApplicationHeader.qml +OverlayDrawer 2.2 OverlayDrawer.qml +singleton AppHeaderSizeGroup 2.2 SingletonHeaderSizeGroup.qml \ No newline at end of file diff --git a/src/delegaterecycler.cpp b/src/delegaterecycler.cpp new file mode 100644 index 0000000..3d1c0f3 --- /dev/null +++ b/src/delegaterecycler.cpp @@ -0,0 +1,414 @@ +/* + * SPDX-FileCopyrightText: 2011 Marco Martin + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "delegaterecycler.h" + +#include "loggingcategory.h" +#include +#include +#include +#include + +DelegateRecyclerAttached::DelegateRecyclerAttached(QObject *parent) + : QObject(parent) +{ +} + +DelegateRecyclerAttached::~DelegateRecyclerAttached() +{ +} +/* +void setRecycler(DelegateRecycler *recycler) +{ + m_recycler = recycler; +} + +DelegateRecycler *recycler() const +{ + return m_recycler; +} +*/ + +class DelegateCache +{ +public: + DelegateCache(); + ~DelegateCache(); + + void ref(QQmlComponent *); + void deref(QQmlComponent *); + + void insert(QQmlComponent *, QQuickItem *); + QQuickItem *take(QQmlComponent *); + +private: + static const int s_cacheSize = 40; + QHash m_refs; + QHash> m_unusedItems; +}; + +Q_GLOBAL_STATIC(DelegateCache, s_delegateCache) + +DelegateCache::DelegateCache() +{ +} + +DelegateCache::~DelegateCache() +{ + for (auto &item : std::as_const(m_unusedItems)) { + qDeleteAll(item); + } +} + +void DelegateCache::ref(QQmlComponent *component) +{ + m_refs[component]++; +} + +void DelegateCache::deref(QQmlComponent *component) +{ + auto itRef = m_refs.find(component); + if (itRef == m_refs.end()) { + return; + } + + (*itRef)--; + if (*itRef <= 0) { + m_refs.erase(itRef); + + qDeleteAll(m_unusedItems.take(component)); + } +} + +void DelegateCache::insert(QQmlComponent *component, QQuickItem *item) +{ + auto &items = m_unusedItems[component]; + if (items.length() >= s_cacheSize) { + item->deleteLater(); + return; + } + + DelegateRecyclerAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, false)); + if (attached) { + Q_EMIT attached->pooled(); + } + + item->setParentItem(nullptr); + items.append(item); +} + +QQuickItem *DelegateCache::take(QQmlComponent *component) +{ + auto it = m_unusedItems.find(component); + if (it != m_unusedItems.end() && !it->isEmpty()) { + return it->takeFirst(); + } + return nullptr; +} + +DelegateRecycler::DelegateRecycler(QQuickItem *parent) + : QQuickItem(parent) +{ + setFlags(QQuickItem::ItemIsFocusScope); +} + +DelegateRecycler::~DelegateRecycler() +{ + if (m_sourceComponent) { + s_delegateCache->insert(m_sourceComponent, m_item); + s_delegateCache->deref(m_sourceComponent); + } +} + +void DelegateRecycler::syncIndex() +{ + const QVariant newIndex = m_propertiesTracker->property("trackedIndex"); + if (!m_item || !newIndex.isValid()) { + return; + } + QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext(); + ctx->setContextProperty(QStringLiteral("index"), newIndex); +} + +void DelegateRecycler::syncModel() +{ + const QVariant newModel = m_propertiesTracker->property("trackedModel"); + if (!m_item || !newModel.isValid()) { + return; + } + QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext(); + ctx->setContextProperty(QStringLiteral("model"), newModel); + + // try to bind all properties + QObject *modelObj = newModel.value(); + if (modelObj) { + const QMetaObject *metaObj = modelObj->metaObject(); + for (int i = metaObj->propertyOffset(); i < metaObj->propertyCount(); ++i) { + ctx->setContextProperty(QString::fromUtf8(metaObj->property(i).name()), metaObj->property(i).read(modelObj)); + } + } +} + +void DelegateRecycler::syncModelProperties() +{ + const QVariant model = m_propertiesTracker->property("trackedModel"); + if (!m_item || !model.isValid()) { + return; + } + QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext(); + + // try to bind all properties + QObject *modelObj = model.value(); + if (modelObj) { + const QMetaObject *metaObj = modelObj->metaObject(); + for (int i = metaObj->propertyOffset(); i < metaObj->propertyCount(); ++i) { + ctx->setContextProperty(QString::fromUtf8(metaObj->property(i).name()), metaObj->property(i).read(modelObj)); + } + } +} + +void DelegateRecycler::syncModelData() +{ + const QVariant newModelData = m_propertiesTracker->property("trackedModelData"); + if (!m_item || !newModelData.isValid()) { + return; + } + QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext(); + ctx->setContextProperty(QStringLiteral("modelData"), newModelData); +} + +QQmlComponent *DelegateRecycler::sourceComponent() const +{ + return m_sourceComponent; +} + +void DelegateRecycler::setSourceComponent(QQmlComponent *component) +{ + if (component && component->parent() == this) { + qCWarning(KirigamiLog) << "Error: source components cannot be declared inside DelegateRecycler"; + return; + } + if (m_sourceComponent == component) { + return; + } + + if (!m_propertiesTracker) { + static QHash propertiesTrackerComponent; + auto engine = qmlEngine(this); + auto it = propertiesTrackerComponent.find(engine); + if (it == propertiesTrackerComponent.end()) { + connect(engine, &QObject::destroyed, engine, [engine] { + propertiesTrackerComponent.remove(engine); + }); + it = propertiesTrackerComponent.insert(engine, new QQmlComponent(engine, engine)); + + /* clang-format off */ + (*it)->setData(QByteArrayLiteral(R"( +import QtQuick 2.3 +QtObject { + property int trackedIndex: index + property var trackedModel: typeof model != 'undefined' ? model : null + property var trackedModelData: typeof modelData != 'undefined' ? modelData : null +} +)"), QUrl(QStringLiteral("delegaterecycler.cpp"))); + } + /* clang-format on */ + m_propertiesTracker = (*it)->create(QQmlEngine::contextForObject(this)); + + connect(m_propertiesTracker, SIGNAL(trackedIndexChanged()), this, SLOT(syncIndex())); + connect(m_propertiesTracker, SIGNAL(trackedModelChanged()), this, SLOT(syncModel())); + connect(m_propertiesTracker, SIGNAL(trackedModelDataChanged()), this, SLOT(syncModelData())); + } + + if (m_sourceComponent) { + if (m_item) { + disconnect(m_item.data(), &QQuickItem::implicitWidthChanged, this, &DelegateRecycler::updateHints); + disconnect(m_item.data(), &QQuickItem::implicitHeightChanged, this, &DelegateRecycler::updateHints); + s_delegateCache->insert(component, m_item); + } + s_delegateCache->deref(component); + } + + m_sourceComponent = component; + s_delegateCache->ref(component); + + m_item = s_delegateCache->take(component); + + if (!m_item) { + QQuickItem *candidate = parentItem(); + QQmlContext *ctx = nullptr; + if (component->creationContext()) { + ctx = new QQmlContext(component->creationContext()); + } + while (!ctx && candidate) { + QQmlContext *parentCtx = QQmlEngine::contextForObject(candidate); + if (parentCtx) { + ctx = new QQmlContext(parentCtx, candidate); + break; + } else { + candidate = candidate->parentItem(); + } + } + + Q_ASSERT(ctx); + + QObject *contextObjectToSet = nullptr; + { + // Find the first parent that has a context object with a valid translationDomain property, i.e. is a KLocalizedContext + QQmlContext *auxCtx = ctx; + while (auxCtx != nullptr) { + QObject *auxCtxObj = auxCtx->contextObject(); + if (auxCtxObj && auxCtxObj->property("translationDomain").isValid()) { + contextObjectToSet = auxCtxObj; + break; + } + auxCtx = auxCtx->parentContext(); + } + } + if (contextObjectToSet) { + ctx->setContextObject(contextObjectToSet); + } + + QObject *modelObj = m_propertiesTracker->property("trackedModel").value(); + if (modelObj) { + const QMetaObject *metaObj = modelObj->metaObject(); + for (int i = metaObj->propertyOffset(); i < metaObj->propertyCount(); ++i) { + QMetaProperty prop = metaObj->property(i); + ctx->setContextProperty(QString::fromUtf8(prop.name()), prop.read(modelObj)); + if (prop.hasNotifySignal()) { + QMetaMethod updateSlot = metaObject()->method(metaObject()->indexOfSlot("syncModelProperties()")); + connect(modelObj, prop.notifySignal(), this, updateSlot); + } + } + } + + ctx->setContextProperty(QStringLiteral("model"), m_propertiesTracker->property("trackedModel")); + ctx->setContextProperty(QStringLiteral("modelData"), m_propertiesTracker->property("trackedModelData")); + ctx->setContextProperty(QStringLiteral("index"), m_propertiesTracker->property("trackedIndex")); + ctx->setContextProperty(QStringLiteral("delegateRecycler"), this); + + QObject *obj = component->create(ctx); + m_item = qobject_cast(obj); + if (!m_item) { + obj->deleteLater(); + } else { + connect(m_item.data(), &QObject::destroyed, ctx, &QObject::deleteLater); + // if the user binded an explicit width, consider it, otherwise base upon implicit + m_widthFromItem = m_item->width() > 0 && m_item->width() != m_item->implicitWidth(); + m_heightFromItem = m_item->height() > 0 && m_item->height() != m_item->implicitHeight(); + + if (m_widthFromItem && m_heightFromItem) { + connect(m_item.data(), &QQuickItem::heightChanged, this, [this]() { + updateSize(false); + }); + } + } + } else { + syncModel(); + + QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext(); + ctx->setContextProperties({QQmlContext::PropertyPair{QStringLiteral("modelData"), m_propertiesTracker->property("trackedModelData")}, + QQmlContext::PropertyPair{QStringLiteral("index"), m_propertiesTracker->property("trackedIndex")}, + QQmlContext::PropertyPair{QStringLiteral("delegateRecycler"), QVariant::fromValue(this)}}); + + DelegateRecyclerAttached *attached = qobject_cast(qmlAttachedPropertiesObject(m_item, false)); + if (attached) { + Q_EMIT attached->reused(); + } + } + + if (m_item) { + m_item->setParentItem(this); + connect(m_item.data(), &QQuickItem::implicitWidthChanged, this, &DelegateRecycler::updateHints); + connect(m_item.data(), &QQuickItem::implicitHeightChanged, this, &DelegateRecycler::updateHints); + + updateSize(true); + } + + Q_EMIT sourceComponentChanged(); +} + +void DelegateRecycler::resetSourceComponent() +{ + s_delegateCache->deref(m_sourceComponent); + m_sourceComponent = nullptr; +} + +DelegateRecyclerAttached *DelegateRecycler::qmlAttachedProperties(QObject *object) +{ + return new DelegateRecyclerAttached(object); +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +void DelegateRecycler::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +#else +void DelegateRecycler::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +#endif +{ + if (m_item && newGeometry.size() != oldGeometry.size()) { + updateSize(true); + } +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QQuickItem::geometryChanged(newGeometry, oldGeometry); +#else + QQuickItem::geometryChange(newGeometry, oldGeometry); +#endif +} + +void DelegateRecycler::focusInEvent(QFocusEvent *event) +{ + QQuickItem::focusInEvent(event); + if (!m_item) { + return; + } + + m_item->setFocus(event->reason()); +} + +void DelegateRecycler::updateHints() +{ + updateSize(false); +} + +void DelegateRecycler::updateSize(bool parentResized) +{ + if (!m_item) { + return; + } + + const bool needToUpdateWidth = !m_widthFromItem && parentResized && widthValid(); + const bool needToUpdateHeight = !m_heightFromItem && parentResized && heightValid(); + + if (parentResized) { + m_item->setPosition(QPoint(0, 0)); + } + if (needToUpdateWidth && needToUpdateHeight) { + m_item->setSize(QSizeF(width(), height())); + } else if (needToUpdateWidth) { + m_item->setWidth(width()); + } else if (needToUpdateHeight) { + m_item->setHeight(height()); + } + + if (m_updatingSize) { + return; + } + + m_updatingSize = true; + + if (m_heightFromItem) { + setHeight(m_item->height()); + } + if (m_widthFromItem) { + setWidth(m_item->width()); + } + + setImplicitSize(m_item->implicitWidth() >= 0 ? m_item->implicitWidth() : m_item->width(), + m_item->implicitHeight() >= 0 ? m_item->implicitHeight() : m_item->height()); + + m_updatingSize = false; +} diff --git a/src/delegaterecycler.h b/src/delegaterecycler.h new file mode 100644 index 0000000..92527b7 --- /dev/null +++ b/src/delegaterecycler.h @@ -0,0 +1,96 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef DELEGATERECYCLER_H +#define DELEGATERECYCLER_H + +#include +#include +#include + +class DelegateRecyclerAttached : public QObject +{ + Q_OBJECT + +public: + DelegateRecyclerAttached(QObject *parent = nullptr); + ~DelegateRecyclerAttached() override; + +Q_SIGNALS: + void pooled(); + void reused(); +}; + +/** + * This class may be used as a delegate of a ListView or a GridView in the case + * the intended delegate is a bit heavy, with many objects inside. + * This will ensure the delegate instances will be put back in a common pool after + * destruction, so when scrolling a big list, the delegates from old delete items will + * be taken from the pool and reused, minimizing the need of instantiating new objects + * and deleting old ones. It ensures scrolling of lists with heavy delegates is + * smoother and helps with memory fragmentations as well. + * + * @note CardListView and CardGridView are already using this recycler, so do NOT use it + * as a delegate for those 2 views. Also, do NOT use this with a Repeater. + * @since 2.4 + */ +class DelegateRecycler : public QQuickItem +{ + Q_OBJECT + + /** + * The Component the actual delegates will be built from. + * + * @note the component may not be a child of this object, therefore it can't be + * declared inside the DelegateRecycler declaration. + * + * The DelegateRecycler will not take ownership of the delegate Component, so it's up + * to the caller to delete it (usually with the normal child/parent relationship) + */ + Q_PROPERTY(QQmlComponent *sourceComponent READ sourceComponent WRITE setSourceComponent RESET resetSourceComponent NOTIFY sourceComponentChanged) + +public: + DelegateRecycler(QQuickItem *parent = nullptr); + ~DelegateRecycler() override; + + QQmlComponent *sourceComponent() const; + void setSourceComponent(QQmlComponent *component); + void resetSourceComponent(); + + static DelegateRecyclerAttached *qmlAttachedProperties(QObject *object); + +protected: +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; +#else + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; +#endif + void focusInEvent(QFocusEvent *event) override; + + void updateHints(); + void updateSize(bool parentResized); + +Q_SIGNALS: + void sourceComponentChanged(); + +private Q_SLOTS: + void syncIndex(); + void syncModel(); + void syncModelProperties(); + void syncModelData(); + +private: + QPointer m_sourceComponent; + QPointer m_item; + QObject *m_propertiesTracker = nullptr; + bool m_updatingSize = false; + bool m_widthFromItem = false; + bool m_heightFromItem = false; +}; + +QML_DECLARE_TYPEINFO(DelegateRecycler, QML_HAS_ATTACHED_PROPERTIES) + +#endif diff --git a/src/enums.cpp b/src/enums.cpp new file mode 100644 index 0000000..3b55546 --- /dev/null +++ b/src/enums.cpp @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "enums.h" + +#include "moc_enums.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/enums.h b/src/enums.h new file mode 100644 index 0000000..1c0ce52 --- /dev/null +++ b/src/enums.h @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef ENUMS_H +#define ENUMS_H + +#include + +class ApplicationHeaderStyle : public QObject +{ + Q_OBJECT + +public: + enum Status { + Auto = 0, + Breadcrumb, + Titles, + TabBar, + ToolBar, ///@since 5.48 + None, ///@since 5.48 + }; + Q_ENUM(Status) + + enum NavigationButton { + NoNavigationButtons = 0, + ShowBackButton = 0x1, + ShowForwardButton = 0x2, + }; + Q_ENUM(NavigationButton) + Q_DECLARE_FLAGS(NavigationButtons, NavigationButton) +}; + +class MessageType : public QObject +{ + Q_OBJECT + +public: + enum Type { + Information = 0, + Positive, + Warning, + Error, + }; + Q_ENUM(Type) +}; + +class DisplayHint : public QObject +{ + Q_OBJECT + +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 // ENUMS_H diff --git a/src/formlayoutattached.cpp b/src/formlayoutattached.cpp new file mode 100644 index 0000000..51e644d --- /dev/null +++ b/src/formlayoutattached.cpp @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "formlayoutattached.h" +#include +#include + +FormLayoutAttached::FormLayoutAttached(QObject *parent) + : QObject(parent) +{ + m_buddyFor = qobject_cast(parent); +} + +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 section) +{ + if (m_labelAlignment == section) { + return; + } + + m_labelAlignment = section; + 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; +} + +void FormLayoutAttached::setCheckable(bool checkable) +{ + if (checkable == m_checkable) { + return; + } + + m_checkable = checkable; + Q_EMIT checkableChanged(); +} + +bool FormLayoutAttached::checkable() const +{ + return m_checkable; +} + +void FormLayoutAttached::setChecked(bool checked) +{ + if (checked == m_checked) { + return; + } + + m_checked = checked; + Q_EMIT checkedChanged(); +} + +bool FormLayoutAttached::checked() const +{ + return m_checked; +} + +void FormLayoutAttached::setEnabled(bool enabled) +{ + if (enabled == m_enabled) { + return; + } + + m_enabled = enabled; + Q_EMIT enabledChanged(); +} + +bool FormLayoutAttached::enabled() const +{ + return m_enabled; +} + +QQuickItem *FormLayoutAttached::buddyFor() const +{ + return m_buddyFor; +} + +void FormLayoutAttached::setBuddyFor(QQuickItem *buddyfor) +{ + if (m_buddyFor == buddyfor || !m_buddyFor->isAncestorOf(buddyfor)) { + return; + } + + m_buddyFor = buddyfor; + Q_EMIT buddyForChanged(); +} + +FormLayoutAttached *FormLayoutAttached::qmlAttachedProperties(QObject *object) +{ + return new FormLayoutAttached(object); +} + +#include "moc_formlayoutattached.cpp" diff --git a/src/formlayoutattached.h b/src/formlayoutattached.h new file mode 100644 index 0000000..2f39094 --- /dev/null +++ b/src/formlayoutattached.h @@ -0,0 +1,192 @@ +/* + * 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 + * import org.kde.kirigami 2.3 as Kirigami + * Kirigami.FormLayout { + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * } + * @endcode + * @see org::kde::kirigami::FormLayout + * @since 2.3 + */ +class FormLayoutAttached : public QObject +{ + Q_OBJECT + /** + * The label for a org::kde::kirigami::FormLayout field + */ + Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged) + /** + * The alignment for the label of a org::kde::kirigami::FormLayout field + */ + Q_PROPERTY(int labelAlignment READ labelAlignment WRITE setLabelAlignment NOTIFY labelAlignmentChanged) + /** + * 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) + + /** + * If true, a checkbox is prepended to the org::kde::kirigami::FormLayout item. + */ + Q_PROPERTY(bool checkable READ checkable WRITE setCheckable NOTIFY checkableChanged) + + /** + * This property is true when the checkbox of the org::kde::kirigami::FormLayout item is checked. + * @see checkable. + */ + Q_PROPERTY(bool checked READ checked WRITE setChecked NOTIFY checkedChanged) + + /** + * This property holds whether the label and the checkbox of the org::kde::kirigami::FormLayout item receive mouse and keyboard events. + */ + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + + /** + * 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. + * + * @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) + +public: + explicit FormLayoutAttached(QObject *parent = nullptr); + ~FormLayoutAttached() override; + + void setLabel(const QString &text); + QString label() const; + + void setIsSection(bool section); + bool isSection() const; + + void setCheckable(bool checkable); + bool checkable() const; + + void setChecked(bool checked); + bool checked() const; + + void setEnabled(bool enabled); + bool enabled() const; + + QQuickItem *buddyFor() const; + void setBuddyFor(QQuickItem *buddyfor); + + int labelAlignment() const; + void setLabelAlignment(int alignment); + + // QML attached property + static FormLayoutAttached *qmlAttachedProperties(QObject *object); + +Q_SIGNALS: + void labelChanged(); + void isSectionChanged(); + void checkableChanged(); + void checkedChanged(); + void enabledChanged(); + void buddyForChanged(); + void labelAlignmentChanged(); + +private: + QString m_label; + QString m_actualDecoratedLabel; + QString m_decoratedLabel; + QPointer m_buddyFor; + bool m_isSection = false; + bool m_checkable = false; + bool m_checked = false; + bool m_enabled = true; + int m_labelAlignment = 0; +}; + +QML_DECLARE_TYPEINFO(FormLayoutAttached, QML_HAS_ATTACHED_PROPERTIES) + +#endif // FORMLAYOUTATTACHED_H diff --git a/src/icon.cpp b/src/icon.cpp new file mode 100644 index 0000000..21ee12e --- /dev/null +++ b/src/icon.cpp @@ -0,0 +1,626 @@ +/* + * SPDX-FileCopyrightText: 2011 Marco Martin + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "icon.h" +#include "libkirigami/platformtheme.h" +#include "scenegraph/managedtexturenode.h" + +#include "loggingcategory.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_GLOBAL_STATIC(ImageTexturesCache, s_iconImageCache) + +Icon::Icon(QQuickItem *parent) + : QQuickItem(parent) + , m_changed(false) + , 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); + // FIXME: not necessary anymore + connect(qApp, &QGuiApplication::paletteChanged, this, &QQuickItem::polish); + connect(this, &QQuickItem::enabledChanged, this, &QQuickItem::polish); + connect(this, &QQuickItem::smoothChanged, this, &QQuickItem::polish); +} + +Icon::~Icon() +{ +} + +void Icon::setSource(const QVariant &icon) +{ + if (m_source == icon) { + return; + } + m_source = icon; + m_monochromeHeuristics.clear(); + + if (!m_theme) { + m_theme = static_cast(qmlAttachedPropertiesObject(this, true)); + Q_ASSERT(m_theme); + + connect(m_theme, &Kirigami::PlatformTheme::colorsChanged, this, &QQuickItem::polish); + } + + if (icon.type() == QVariant::String) { + const QString iconSource = icon.toString(); + m_isMaskHeuristic = (iconSource.endsWith(QLatin1String("-symbolic")) // + || iconSource.endsWith(QLatin1String("-symbolic-rtl")) // + || iconSource.endsWith(QLatin1String("-symbolic-ltr"))); + Q_EMIT isMaskChanged(); + } + + 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; + m_isMaskHeuristic = mask; + polish(); + Q_EMIT isMaskChanged(); +} + +bool Icon::isMask() const +{ + return m_isMask || m_isMaskHeuristic; +} + +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::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData * /*data*/) +{ + if (m_source.isNull() || qFuzzyIsNull(width()) || qFuzzyIsNull(height())) { + delete node; + return Q_NULLPTR; + } + + if (m_changed || node == nullptr) { + const QSize itemSize(width(), height()); + QRect nodeRect(QPoint(0, 0), itemSize); + + ManagedTextureNode *mNode = dynamic_cast(node); + if (!mNode) { + delete node; + mNode = new ManagedTextureNode; + } + if (itemSize.width() != 0 && itemSize.height() != 0) { + const auto multiplier = QCoreApplication::instance()->testAttribute(Qt::AA_UseHighDpiPixmaps) + ? 1 + : (window() ? window()->devicePixelRatio() : qGuiApp->devicePixelRatio()); + const QSize size = itemSize * multiplier; + mNode->setTexture(s_iconImageCache->loadTexture(window(), m_icon, QQuickWindow::TextureCanUseAtlas)); + if (m_icon.size() != size) { + // 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: + QRect destination(QPoint(0, 0), m_icon.size().scaled(itemSize, Qt::KeepAspectRatio)); + destination.moveCenter(nodeRect.center()); + nodeRect = destination; + } + } + mNode->setRect(nodeRect); + node = mNode; + if (smooth()) { + mNode->setFiltering(QSGTexture::Linear); + } + m_changed = false; + } + + return node; +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +void Icon::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + QQuickItem::geometryChanged(newGeometry, oldGeometry); +#else +void Icon::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + QQuickItem::geometryChange(newGeometry, oldGeometry); +#endif + if (newGeometry.size() != oldGeometry.size()) { + 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())) { + qCWarning(KirigamiLog) << "received broken image" << reply->url(); + + // broken image from data, inform the user of this with some useful broken-image thing... + const QIcon icon = QIcon::fromTheme(m_fallback); + m_loadedImage = icon.pixmap(window(), icon.actualSize(size().toSize()), iconMode(), QIcon::On).toImage(); + } + + polish(); +} + +void Icon::updatePolish() +{ + QQuickItem::updatePolish(); + + if (m_source.isNull()) { + setStatus(Ready); + updatePaintedGeometry(); + update(); + return; + } + + const QSize itemSize(width(), height()); + if (itemSize.width() != 0 && itemSize.height() != 0) { + const auto multiplier = + QCoreApplication::instance()->testAttribute(Qt::AA_UseHighDpiPixmaps) ? 1 : (window() ? window()->devicePixelRatio() : qGuiApp->devicePixelRatio()); + const QSize size = itemSize * multiplier; + + switch (m_source.type()) { + case QVariant::Pixmap: + m_icon = m_source.value().toImage(); + break; + case QVariant::Image: + m_icon = m_source.value(); + break; + case QVariant::Bitmap: + m_icon = m_source.value().toImage(); + break; + case QVariant::Icon: { + const QIcon icon = m_source.value(); + m_icon = icon.pixmap(window(), icon.actualSize(itemSize), iconMode(), QIcon::On).toImage(); + break; + } + case QVariant::Url: + case QVariant::String: + m_icon = findIcon(size); + break; + case QVariant::Brush: + // todo: fill here too? + case QVariant::Color: + 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() || guessMonochrome(m_icon))) { + QPainter p(&m_icon); + p.setCompositionMode(QPainter::CompositionMode_SourceIn); + p.fillRect(m_icon.rect(), tintColor); + p.end(); + } + } + m_changed = true; + updatePaintedGeometry(); + update(); +} + +QImage Icon::findIcon(const QSize &size) +{ + QImage img; + QString iconSource = m_source.toString(); + + if (iconSource.startsWith(QLatin1String("image://"))) { + const auto multiplier = + QCoreApplication::instance()->testAttribute(Qt::AA_UseHighDpiPixmaps) ? (window() ? window()->devicePixelRatio() : qGuiApp->devicePixelRatio()) : 1; + 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; + QQuickImageProvider *imageProvider = dynamic_cast(qmlEngine(this)->imageProvider(iconProviderId)); + if (!imageProvider) { + return img; + } + switch (imageProvider->imageType()) { + case QQmlImageProviderBase::Image: + img = imageProvider->requestImage(iconId, &actualSize, size * multiplier); + if (!img.isNull()) { + setStatus(Ready); + } + break; + case QQmlImageProviderBase::Pixmap: + img = imageProvider->requestPixmap(iconId, &actualSize, size * multiplier).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 * multiplier); + 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... + const QIcon icon = QIcon::fromTheme(m_fallback); + m_loadedImage = icon.pixmap(window(), icon.actualSize(QSize(width(), height())), iconMode(), QIcon::On).toImage(); + setStatus(Error); + } else { + setStatus(Ready); + } + polish(); + } + response->deleteLater(); + }); + // Temporary icon while we wait for the real image to load... + const QIcon icon = QIcon::fromTheme(m_placeholder); + img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage(); + break; + } + case QQmlImageProviderBase::Texture: { + QQuickTextureFactory *textureFactory = imageProvider->requestTexture(iconId, &actualSize, size * multiplier); + 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... + const QIcon icon = QIcon::fromTheme(m_fallback); + img = icon.pixmap(window(), icon.actualSize(QSize(width(), height())), iconMode(), QIcon::On).toImage(); + 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... + const QIcon icon = QIcon::fromTheme(m_placeholder); + img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage(); + } else { + if (iconSource.startsWith(QLatin1String("qrc:/"))) { + iconSource = iconSource.mid(3); + } else if (iconSource.startsWith(QLatin1String("file:/"))) { + iconSource = QUrl(iconSource).path(); + } + + QIcon icon; + const bool isPath = iconSource.contains(QLatin1String("/")); + if (isPath) { + icon = QIcon(iconSource); + } else { + if (icon.isNull()) { + icon = m_theme->iconFromTheme(iconSource, m_color); + } + } + if (!icon.isNull()) { + img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage(); + + setStatus(Ready); + /*const QColor tintColor = !m_color.isValid() || m_color == Qt::transparent ? (m_selected ? m_theme->highlightedTextColor() : m_theme->textColor()) + : m_color; + + if (m_isMask || icon.isMask() || iconSource.endsWith(QLatin1String("-symbolic")) || iconSource.endsWith(QLatin1String("-symbolic-rtl")) || + iconSource.endsWith(QLatin1String("-symbolic-ltr")) || guessMonochrome(img)) { // + QPainter p(&img); + p.setCompositionMode(QPainter::CompositionMode_SourceIn); + p.fillRect(img.rect(), tintColor); + p.end(); + }*/ + } + } + + if (!iconSource.isEmpty() && img.isNull()) { + setStatus(Error); + const QIcon icon = QIcon::fromTheme(m_fallback); + img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage(); + } + 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; +} + +bool Icon::guessMonochrome(const QImage &img) +{ + // don't try for too big images + if (img.width() >= 256 || m_theme->supportsIconColoring()) { + return false; + } + // round size to a standard size. hardcode as we can't use KIconLoader + int stdSize; + if (img.width() <= 16) { + stdSize = 16; + } else if (img.width() <= 22) { + stdSize = 22; + } else if (img.width() <= 24) { + stdSize = 24; + } else if (img.width() <= 32) { + stdSize = 32; + } else if (img.width() <= 48) { + stdSize = 48; + } else if (img.width() <= 64) { + stdSize = 64; + } else { + stdSize = 128; + } + + auto findIt = m_monochromeHeuristics.constFind(stdSize); + if (findIt != m_monochromeHeuristics.constEnd()) { + return findIt.value(); + } + + QHash dist; + int transparentPixels = 0; + int saturatedPixels = 0; + for (int x = 0; x < img.width(); x++) { + for (int y = 0; y < img.height(); y++) { + QColor color = QColor::fromRgba(qUnpremultiply(img.pixel(x, y))); + if (color.alpha() < 100) { + ++transparentPixels; + continue; + } else if (color.saturation() > 84) { + ++saturatedPixels; + } + dist[qGray(color.rgb())]++; + } + } + + QMultiMap reverseDist; + auto it = dist.constBegin(); + qreal entropy = 0; + while (it != dist.constEnd()) { + reverseDist.insert(it.value(), it.key()); + qreal probability = qreal(it.value()) / qreal(img.size().width() * img.size().height() - transparentPixels); + entropy -= probability * log(probability) / log(255); + ++it; + } + + // Arbitrarily low values of entropy and colored pixels + m_monochromeHeuristics[stdSize] = saturatedPixels <= (img.size().width() * img.size().height() - transparentPixels) * 0.3 && entropy <= 0.3; + return m_monochromeHeuristics[stdSize]; +} + +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 m_paintedWidth; +} + +qreal Icon::paintedHeight() const +{ + return m_paintedHeight; +} + +void Icon::updatePaintedGeometry() +{ + qreal newWidth = 0.0; + qreal newHeight = 0.0; + if (!m_icon.width() || !m_icon.height()) { + newWidth = newHeight = 0.0; + } else { + const qreal w = widthValid() ? width() : m_icon.size().width(); + const qreal widthScale = w / m_icon.size().width(); + const qreal h = heightValid() ? height() : m_icon.size().height(); + const qreal heightScale = h / m_icon.size().height(); + if (widthScale <= heightScale) { + newWidth = w; + newHeight = widthScale * m_icon.size().height(); + } else if (heightScale < widthScale) { + newWidth = heightScale * m_icon.size().width(); + newHeight = h; + } + } + if (newWidth != m_paintedWidth || newHeight != m_paintedHeight) { + m_paintedWidth = newWidth; + m_paintedHeight = newHeight; + Q_EMIT paintedAreaChanged(); + } +} + +#include "moc_icon.cpp" diff --git a/src/icon.h b/src/icon.h new file mode 100644 index 0000000..6262fc5 --- /dev/null +++ b/src/icon.h @@ -0,0 +1,243 @@ +/* + * 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 + +class QNetworkReply; + +namespace Kirigami +{ +class PlatformTheme; +} + +/** + * Class for rendering an icon in UI. + */ +class Icon : public QQuickItem +{ + Q_OBJECT + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * Whether this icon's `source` is valid and it is being used. + */ + Q_PROPERTY(bool valid READ valid NOTIFY validChanged) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) +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 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; + + 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(); + +protected: +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; +#else + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; +#endif + 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(); + +private: + Kirigami::PlatformTheme *m_theme = nullptr; + QPointer m_networkReply; + QHash m_monochromeHeuristics; + QVariant m_source; + Status m_status = Null; + bool m_changed; + 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"); + qreal m_paintedWidth = 0.0; + qreal m_paintedHeight = 0.0; + + QImage m_icon; +}; diff --git a/src/imagecolors.cpp b/src/imagecolors.cpp new file mode 100644 index 0000000..e87e3c9 --- /dev/null +++ b/src/imagecolors.cpp @@ -0,0 +1,491 @@ +/* + * Copyright 2020 Marco Martin + * + * 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 2.010-1301, USA. + */ + +#include "imagecolors.h" +#include "platformtheme.h" + +#include +#include +#include + +#include "loggingcategory.h" +#include +#include + +#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(); \ + } + +ImageColors::ImageColors(QObject *parent) + : QObject(parent) +{ + m_imageSyncTimer = new QTimer(this); + m_imageSyncTimer->setSingleShot(true); + m_imageSyncTimer->setInterval(100); + /* connect(m_imageSyncTimer, &QTimer::timeout, this, [this]() { + generatePalette(); + });*/ +} + +ImageColors::~ImageColors() +{ +} + +void ImageColors::setSource(const QVariant &source) +{ + 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()) { + setSourceImage(QIcon::fromTheme(source.toString()).pixmap(128, 128).toImage()); + } 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); + } + }; + + connect(m_sourceItem, &QQuickItem::windowChanged, this, syncWindow); + syncWindow(); + } +} + +QQuickItem *ImageColors::sourceItem() const +{ + return m_sourceItem; +} + +void ImageColors::update() +{ + if (m_futureImageData) { + m_futureImageData->cancel(); + m_futureImageData->deleteLater(); + } + auto runUpdate = [this]() { + QFuture future = QtConcurrent::run([this]() { + return generatePalette(m_sourceImage); + }); + m_futureImageData = new QFutureWatcher(this); + connect(m_futureImageData, &QFutureWatcher::finished, this, [this]() { + if (!m_futureImageData) { + return; + } + m_imageData = m_futureImageData->future().result(); + m_futureImageData->deleteLater(); + m_futureImageData = nullptr; + + Q_EMIT paletteChanged(); + }); + m_futureImageData->setFuture(future); + }; + + if (!m_sourceItem || !m_window) { + if (!m_sourceImage.isNull()) { + runUpdate(); + } + 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(); + }); + } +} + +inline int squareDistance(QRgb color1, QRgb color2) +{ + // https://en.wikipedia.org/wiki/Color_difference + // Using RGB distance for performance, as CIEDE2000 istoo 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; +} + +ImageData ImageColors::generatePalette(const QImage &sourceImage) +{ + ImageData imageData; + + if (sourceImage.isNull() || sourceImage.width() == 0) { + return imageData; + } + + imageData.m_clusters.clear(); + imageData.m_samples.clear(); + + QColor sampleColor; + int r = 0; + int g = 0; + int b = 0; + int c = 0; + for (int x = 0; x < sourceImage.width(); ++x) { + for (int y = 0; y < sourceImage.height(); ++y) { + sampleColor = sourceImage.pixelColor(x, y); + if (sampleColor.alpha() == 0) { + continue; + } + QRgb rgb = sampleColor.rgb(); + c++; + r += qRed(rgb); + g += qGreen(rgb); + b += qBlue(rgb); + imageData.m_samples << rgb; + positionColor(rgb, imageData.m_clusters); + } + } + + if (imageData.m_samples.isEmpty()) { + return imageData; + } + + imageData.m_average = QColor(r / c, g / c, b / c, 255); + + for (int iteration = 0; iteration < 5; ++iteration) { + for (auto &stat : imageData.m_clusters) { + 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 = qreal(stat.colors.count()) / qreal(imageData.m_samples.count()); + stat.colors = QList({stat.centroid}); + } + + for (auto color : std::as_const(imageData.m_samples)) { + positionColor(color, imageData.m_clusters); + } + } + + std::sort(imageData.m_clusters.begin(), imageData.m_clusters.end(), [](const ImageData::colorStat &a, const ImageData::colorStat &b) { + return a.colors.size() > b.colors.size(); + }); + + // 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; + + for (const auto &stat : std::as_const(imageData.m_clusters)) { + QVariantMap entry; + const QColor color(stat.centroid); + entry[QStringLiteral("color")] = color; + entry[QStringLiteral("ratio")] = stat.ratio; + + 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)); + } + + entry[QStringLiteral("contrastColor")] = contrast; + + 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 << entry; + } + + return imageData; +} + +QVariantList 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..8bada5c --- /dev/null +++ b/src/imagecolors.h @@ -0,0 +1,278 @@ +/* + * Copyright 2020 Marco Martin + * + * 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 2.010-1301, USA. + */ + +#pragma once + +#include "colorutils.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +class QTimer; + +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; + QVariantList m_palette; + + bool m_darkPalette = true; + QColor m_dominant; + 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 + /** + * 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) + + /** + * 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(QVariantList palette READ palette NOTIFY paletteChanged) + + /** + * 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) + + /** + * The average color of the source image. + */ + Q_PROPERTY(QColor average READ average NOTIFY paletteChanged) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * The lightest color of the source image. + */ + Q_PROPERTY(QColor closestToWhite READ closestToWhite NOTIFY paletteChanged) + + /** + * The darkest color of the source image. + */ + Q_PROPERTY(QColor closestToBlack READ closestToBlack NOTIFY paletteChanged) + + /** + * The value to return when palette is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(QVariantList fallbackPalette MEMBER m_fallbackPalette NOTIFY fallbackPaletteChanged) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + +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(); + + QVariantList 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 ImageData generatePalette(const QImage &sourceImage); + + // 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; + + QTimer *m_imageSyncTimer; + + QFutureWatcher *m_futureImageData = nullptr; + ImageData m_imageData; + + QVariantList 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/inputmethod.cpp b/src/inputmethod.cpp new file mode 100644 index 0000000..cf3bc42 --- /dev/null +++ b/src/inputmethod.cpp @@ -0,0 +1,101 @@ +/* + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "inputmethod.h" + +#include "libkirigami/virtualkeyboardwatcher.h" + +class Q_DECL_HIDDEN 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 = Kirigami::VirtualKeyboardWatcher::self(); + + connect(watcher, &Kirigami::VirtualKeyboardWatcher::availableChanged, this, [this]() { + d->available = Kirigami::VirtualKeyboardWatcher::self()->available(); + Q_EMIT availableChanged(); + }); + + connect(watcher, &Kirigami::VirtualKeyboardWatcher::enabledChanged, this, [this]() { + d->enabled = Kirigami::VirtualKeyboardWatcher::self()->enabled(); + Q_EMIT enabledChanged(); + }); + + connect(watcher, &Kirigami::VirtualKeyboardWatcher::activeChanged, this, [this]() { + d->active = Kirigami::VirtualKeyboardWatcher::self()->active(); + Q_EMIT activeChanged(); + }); + + connect(watcher, &Kirigami::VirtualKeyboardWatcher::visibleChanged, this, [this]() { + d->visible = Kirigami::VirtualKeyboardWatcher::self()->visible(); + Q_EMIT visibleChanged(); + }); + + connect(watcher, &Kirigami::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; +} + +void InputMethod::setEnabled(bool newEnabled) +{ + if (newEnabled == d->enabled) { + return; + } + + d->enabled = newEnabled; + Q_EMIT enabledChanged(); +} + +bool InputMethod::active() const +{ + return d->active; +} + +void InputMethod::setActive(bool newActive) +{ + if (newActive == d->active) { + return; + } + + d->active = newActive; + Q_EMIT activeChanged(); +} + +bool InputMethod::visible() const +{ + return d->visible; +} + +bool InputMethod::willShowOnActive() const +{ + return Kirigami::VirtualKeyboardWatcher::self()->willShowOnActive(); +} diff --git a/src/inputmethod.h b/src/inputmethod.h new file mode 100644 index 0000000..c7114a5 --- /dev/null +++ b/src/inputmethod.h @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef INPUTMETHOD_H +#define INPUTMETHOD_H + +#include + +#include + +/** + * This exposes information about the current used input method. + */ +class InputMethod : public QObject +{ + Q_OBJECT + +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) + 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 WRITE setEnabled NOTIFY enabledChanged) + bool enabled() const; + void setEnabled(bool newEnabled); + 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 WRITE setActive NOTIFY activeChanged) + bool active() const; + void setActive(bool newActive); + 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) + 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) + bool willShowOnActive() const; + Q_SIGNAL void willShowOnActiveChanged(); + +private: + class Private; + const std::unique_ptr d; +}; + +#endif // INPUTMETHOD_H diff --git a/src/kirigamiplugin.cpp b/src/kirigamiplugin.cpp new file mode 100644 index 0000000..e8f8241 --- /dev/null +++ b/src/kirigamiplugin.cpp @@ -0,0 +1,360 @@ +/* + * 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 "avatar.h" +#include "colorutils.h" +#include "columnview.h" +#include "delegaterecycler.h" +#include "enums.h" +#include "formlayoutattached.h" +#include "icon.h" +#include "imagecolors.h" +#include "inputmethod.h" +#include "mnemonicattached.h" +#include "pagepool.h" +#include "pagerouter.h" +#include "scenepositionattached.h" +#include "settings.h" +#include "shadowedrectangle.h" +#include "shadowedtexture.h" +#include "sizegroup.h" +#include "spellcheckinghint.h" +#include "toolbarlayout.h" +#include "units.h" +#include "wheelhandler.h" + +#include +#include +#include +#include +#include + +#include "libkirigami/basictheme_p.h" +#include "libkirigami/platformtheme.h" +#include "libkirigami/styleselector_p.h" +#include "loggingcategory.h" +#include "libkirigami/basictheme_p.h" +#include "libkirigami/kirigamipluginfactory.h" + +static QString s_selectedStyle; + +#ifdef KIRIGAMI_BUILD_TYPE_STATIC +#include "loggingcategory.h" +#include +#endif + +class CopyHelperPrivate : public QObject +{ + Q_OBJECT +public: + Q_INVOKABLE static void copyTextToClipboard(const QString &text) + { + qGuiApp->clipboard()->setText(text); + } +}; + +// 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) +{ + 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::StyleSelector::componentUrl(fileName); +} + +using SingletonCreationFunction = QObject *(*)(QQmlEngine *, QJSEngine *); + +template +inline SingletonCreationFunction singleton() +{ + return [](QQmlEngine *, QJSEngine *) -> QObject * { + return new T; + }; +} + +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::StyleSelector::setBaseUrl(baseUrl()); + + if (QIcon::themeName().isEmpty() && !qEnvironmentVariableIsSet("XDG_CURRENT_DESKTOP")) { + QIcon::setThemeSearchPaths({Kirigami::StyleSelector::resolveFilePath(QStringLiteral(".")), QStringLiteral(":/icons")}); + QIcon::setThemeName(QStringLiteral("breeze-internal")); + } + + qmlRegisterSingletonType(uri, 2, 0, "Settings", [](QQmlEngine *e, QJSEngine *) -> QObject * { + Settings *settings = Settings::self(); + // singleton managed internally, qml should never delete it + e->setObjectOwnership(settings, QQmlEngine::CppOwnership); + settings->setStyle(Kirigami::StyleSelector::style()); + return settings; + }); + + qmlRegisterUncreatableType(uri, + 2, + 0, + "ApplicationHeaderStyle", + QStringLiteral("Cannot create objects of type ApplicationHeaderStyle")); + + // old legacy retrocompatible Theme + qmlRegisterSingletonType(uri, 2, 0, "Theme", [](QQmlEngine *, QJSEngine *) { + qCWarning(KirigamiLog) << "The Theme singleton is deprecated (since 5.39). Import Kirigami 2.2 or higher and use the attached property instead."; + return new Kirigami::BasicThemeDefinition{}; + }); + + qmlRegisterSingletonType(uri, 2, 0, "Units", [] (QQmlEngine *engine, QJSEngine *) { +#ifndef KIRIGAMI_BUILD_TYPE_STATIC + auto plugin = Kirigami::KirigamiPluginFactory::findPlugin(); + if (plugin) { + // Check if the plugin implements units + auto pluginV2 = qobject_cast(plugin); + if (pluginV2) { + auto units = pluginV2->createUnits(engine); + if (units) { + return units; + } else { + qWarning(KirigamiLog) << "The style returned a nullptr Units*, falling back to defaults"; + } + } else { + qWarning(KirigamiLog) << "The style does not provide a C++ Units implementation." + << "QML Units implementations are no longer supported."; + } + } else { + qWarning(KirigamiLog) << "Failed to find a Kirigami platform plugin"; + } +#endif + // Fall back to the default units implementation + return new Kirigami::Units(engine); + }); + + 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("AbstractListItem.qml")), uri, 2, 0, "AbstractListItem"); + qmlRegisterType(componentUrl(QStringLiteral("ApplicationHeader.qml")), uri, 2, 0, "ApplicationHeader"); + qmlRegisterType(componentUrl(QStringLiteral("ToolBarApplicationHeader.qml")), uri, 2, 0, "ToolBarApplicationHeader"); + qmlRegisterType(componentUrl(QStringLiteral("ApplicationWindow.qml")), uri, 2, 0, "ApplicationWindow"); + qmlRegisterType(componentUrl(QStringLiteral("BasicListItem.qml")), uri, 2, 0, "BasicListItem"); + 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("Separator.qml")), uri, 2, 0, "Separator"); + qmlRegisterType(componentUrl(QStringLiteral("PageRow.qml")), uri, 2, 0, "PageRow"); + + qmlRegisterType(uri, 2, 0, "Icon"); + + qmlRegisterType(componentUrl(QStringLiteral("Label.qml")), uri, 2, 0, "Label"); + // TODO: uncomment for 2.3 release + // qmlRegisterTypeNotAvailable(uri, 2, 3, "Label", "Label type not supported anymore, use QtQuick.Controls.Label 2.0 instead"); + 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("SplitDrawer.qml")), uri, 2, 0, "SplitDrawer"); + qmlRegisterType(componentUrl(QStringLiteral("SwipeListItem.qml")), uri, 2, 0, "SwipeListItem"); + + // 2.1 + qmlRegisterType(componentUrl(QStringLiteral("AbstractItemViewHeader.qml")), uri, 2, 1, "AbstractItemViewHeader"); + qmlRegisterType(componentUrl(QStringLiteral("ItemViewHeader.qml")), uri, 2, 1, "ItemViewHeader"); + qmlRegisterType(componentUrl(QStringLiteral("AbstractApplicationItem.qml")), uri, 2, 1, "AbstractApplicationItem"); + qmlRegisterType(componentUrl(QStringLiteral("ApplicationItem.qml")), uri, 2, 1, "ApplicationItem"); + + // 2.2 + // Theme changed from a singleton to an attached property + qmlRegisterUncreatableType(uri, + 2, + 2, + "Theme", + QStringLiteral("Cannot create objects of type Theme, use it as an attached property")); + + // 2.3 + qmlRegisterType(componentUrl(QStringLiteral("FormLayout.qml")), uri, 2, 3, "FormLayout"); + qmlRegisterUncreatableType(uri, + 2, + 3, + "FormData", + QStringLiteral("Cannot create objects of type FormData, use it as an attached property")); + qmlRegisterUncreatableType(uri, + 2, + 3, + "MnemonicData", + QStringLiteral("Cannot create objects of type MnemonicData, use it as an attached property")); + + // 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("CardsGridView.qml")), uri, 2, 4, "CardsGridView"); + qmlRegisterType(componentUrl(QStringLiteral("CardsLayout.qml")), uri, 2, 4, "CardsLayout"); + qmlRegisterType(componentUrl(QStringLiteral("InlineMessage.qml")), uri, 2, 4, "InlineMessage"); + qmlRegisterUncreatableType(uri, 2, 4, "MessageType", QStringLiteral("Cannot create objects of type MessageType")); + qmlRegisterType(uri, 2, 4, "DelegateRecycler"); + + // 2.5 + qmlRegisterType(componentUrl(QStringLiteral("ListItemDragHandle.qml")), uri, 2, 5, "ListItemDragHandle"); + qmlRegisterType(componentUrl(QStringLiteral("ActionToolBar.qml")), uri, 2, 5, "ActionToolBar"); + qmlRegisterUncreatableType(uri, + 2, + 5, + "ScenePosition", + QStringLiteral("Cannot create objects of type ScenePosition, use it as an attached property")); + + // 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"); + qmlRegisterSingletonType("org.kde.kirigami.private", 2, 6, "CopyHelperPrivate", singleton()); + + // 2.7 + qmlRegisterType(uri, 2, 7, "ColumnView"); + 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.9 + qmlRegisterType(uri, 2, 9, "WheelHandler"); + qmlRegisterUncreatableType(uri, 2, 9, "WheelEvent", QStringLiteral("Cannot create objects of type WheelEvent.")); + + // 2.10 + qmlRegisterType(componentUrl(QStringLiteral("ListSectionHeader.qml")), uri, 2, 10, "ListSectionHeader"); + + // 2.11 + qmlRegisterType(uri, 2, 11, "PagePool"); + qmlRegisterType(componentUrl(QStringLiteral("PagePoolAction.qml")), uri, 2, 11, "PagePoolAction"); + + // TODO: remove + qmlRegisterType(componentUrl(QStringLiteral("SwipeListItem2.qml")), uri, 2, 11, "SwipeListItem2"); + + // 2.12 + qmlRegisterType(uri, 2, 12, "ShadowedRectangle"); + qmlRegisterType(uri, 2, 12, "ShadowedTexture"); + qmlRegisterType(componentUrl(QStringLiteral("ShadowedImage.qml")), uri, 2, 12, "ShadowedImage"); + qmlRegisterType(componentUrl(QStringLiteral("PlaceholderMessage.qml")), uri, 2, 12, "PlaceholderMessage"); + + qmlRegisterUncreatableType(uri, 2, 12, "BorderGroup", QStringLiteral("Used as grouped property")); + qmlRegisterUncreatableType(uri, 2, 12, "ShadowGroup", QStringLiteral("Used as grouped property")); + qmlRegisterSingletonType(uri, 2, 12, "ColorUtils", singleton()); + + qmlRegisterUncreatableType(uri, 2, 12, "CornersGroup", QStringLiteral("Used as grouped property")); + qmlRegisterType(uri, 2, 12, "PageRouter"); + qmlRegisterType(uri, 2, 12, "PageRoute"); + qmlRegisterUncreatableType(uri, 2, 12, "PageRouterAttached", QStringLiteral("PageRouterAttached cannot be created")); + qmlRegisterType(componentUrl(QStringLiteral("RouterWindow.qml")), uri, 2, 12, "RouterWindow"); + + // 2.13 + qmlRegisterType(uri, 2, 13, "ImageColors"); + qmlRegisterType(componentUrl(QStringLiteral("Avatar.qml")), uri, 2, 13, "Avatar"); + qmlRegisterType(componentUrl(QStringLiteral("swipenavigator/SwipeNavigator.qml")), uri, 2, 13, "SwipeNavigator"); + + // 2.14 + qmlRegisterUncreatableType(uri, 2, 14, "PreloadRouteGroup", QStringLiteral("PreloadRouteGroup cannot be created")); + qmlRegisterType(componentUrl(QStringLiteral("FlexColumn.qml")), uri, 2, 14, "FlexColumn"); + qmlRegisterType(uri, 2, 14, "ToolBarLayout"); + qmlRegisterSingletonType(uri, 2, 14, "DisplayHint", singleton()); + qmlRegisterType(uri, 2, 14, "SizeGroup"); + qmlRegisterType("org.kde.kirigami.private", 2, 14, "AvatarGroup"); + qmlRegisterType(componentUrl(QStringLiteral("CheckableListItem.qml")), uri, 2, 14, "CheckableListItem"); + qmlRegisterSingletonType(uri, 2, 14, "NameUtils", singleton()); + + qmlRegisterType(componentUrl(QStringLiteral("Hero.qml")), uri, 2, 15, "Hero"); + + // 2.16 + qmlRegisterType(uri, 2, 16, "BasicThemeDefinition"); + + // 2.17 + qmlRegisterType(componentUrl(QStringLiteral("swipenavigator/TabViewLayout.qml")), uri, 2, 17, "TabViewLayout"); + qmlRegisterType(componentUrl(QStringLiteral("swipenavigator/PageTab.qml")), uri, 2, 17, "PageTab"); + + // 2.18 + qmlRegisterType(uri, 2, 18, "SpellChecking"); + qmlRegisterType(componentUrl(QStringLiteral("settingscomponents/CategorizedSettings.qml")), uri, 2, 18, "CategorizedSettings"); + qmlRegisterType(componentUrl(QStringLiteral("settingscomponents/GenericSettingsPage.qml")), uri, 2, 18, "GenericSettingsPage"); + qmlRegisterType(componentUrl(QStringLiteral("settingscomponents/SettingAction.qml")), uri, 2, 18, "SettingAction"); + + // 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("Dialog.qml")), uri, 2, 19, "Dialog"); + qmlRegisterType(componentUrl(QStringLiteral("MenuDialog.qml")), uri, 2, 19, "MenuDialog"); + qmlRegisterType(componentUrl(QStringLiteral("PromptDialog.qml")), uri, 2, 19, "PromptDialog"); + qmlRegisterType(componentUrl(QStringLiteral("AbstractChip.qml")), uri, 2, 19, "AbstractChip"); + qmlRegisterType(componentUrl(QStringLiteral("Chip.qml")), uri, 2, 19, "Chip"); + qmlRegisterType(componentUrl(QStringLiteral("LoadingPlaceholder.qml")), uri, 2, 19, "LoadingPlaceholder"); + + qmlRegisterSingletonType(uri, 2, 19, "InputMethod", [](QQmlEngine *, QJSEngine *) { + return new InputMethod{}; + }); + + // 2.20 + qmlRegisterType(componentUrl(QStringLiteral("SelectableLabel.qml")), uri, 2, 20, "SelectableLabel"); + + qmlProtectModule(uri, 2); +} + +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) +{ + Q_INIT_RESOURCE(shaders); + Q_INIT_RESOURCE(KirigamiPlugin); + + 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" diff --git a/src/kirigamiplugin.h b/src/kirigamiplugin.h new file mode 100644 index 0000000..8ec815e --- /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/libkirigami/CMakeLists.txt b/src/libkirigami/CMakeLists.txt new file mode 100644 index 0000000..da636bb --- /dev/null +++ b/src/libkirigami/CMakeLists.txt @@ -0,0 +1,161 @@ +set(KIRIGAMI_INSTALL_INCLUDEDIR "${KDE_INSTALL_INCLUDEDIR_KF}/Kirigami2") + +add_library(KF5Kirigami2 ${libkirigami_SRCS}) +add_library(KF5::Kirigami2 ALIAS KF5Kirigami2) + +target_sources(KF5Kirigami2 PRIVATE + platformtheme.cpp + basictheme.cpp + kirigamipluginfactory.cpp + tabletmodewatcher.cpp + styleselector.cpp + units.cpp + virtualkeyboardwatcher.cpp +) + +set(libkirigami_extra_sources "") + +#use dbus on linux, bsd etc, but not android and apple stuff +if (UNIX AND NOT ANDROID AND NOT(APPLE) AND NOT(DISABLE_DBUS)) + qt_add_dbus_interface(libkirigami_extra_sources org.kde.KWin.TabletModeManager.xml tabletmodemanager_interface) + qt_add_dbus_interface(libkirigami_extra_sources org.kde.KWin.VirtualKeyboard.xml virtualkeyboard_interface) + set(LIBKIRIGAMKI_EXTRA_LIBS Qt${QT_MAJOR_VERSION}::DBus) +endif() + +ecm_qt_declare_logging_category(libkirigami_extra_sources + HEADER loggingcategory.h + IDENTIFIER KirigamiLog + CATEGORY_NAME kf.kirigami + DEFAULT_SEVERITY Warning +) + +ecm_generate_export_header(KF5Kirigami2 + VERSION ${KF_VERSION} + BASE_NAME Kirigami2 + DEPRECATION_VERSIONS 5.80 5.86 +) + +target_sources(KF5Kirigami2 PRIVATE ${libkirigami_extra_sources}) + +target_include_directories(KF5Kirigami2 + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} + INTERFACE "$" +) + +target_link_libraries(KF5Kirigami2 + PUBLIC + Qt${QT_MAJOR_VERSION}::Core + PRIVATE + Qt${QT_MAJOR_VERSION}::Qml + Qt${QT_MAJOR_VERSION}::Quick + Qt${QT_MAJOR_VERSION}::QuickControls2 + ${LIBKIRIGAMKI_EXTRA_LIBS} +) + +set_target_properties(KF5Kirigami2 PROPERTIES + VERSION ${KIRIGAMI2_VERSION} + SOVERSION ${KIRIGAMI2_SOVERSION} + EXPORT_NAME "Kirigami2" +) + +ecm_generate_headers(Kirigami2_CamelCase_HEADERS + HEADER_NAMES + PlatformTheme + KirigamiPluginFactory + TabletModeWatcher + Units + VirtualKeyboardWatcher + + PREFIX Kirigami + REQUIRED_HEADERS Kirigami2_HEADERS +) + +if(NOT BUILD_SHARED_LIBS) + ecm_generate_headers(Kirigami2_HEADERS + HEADER_NAMES KirigamiPlugin + REQUIRED_HEADERS Kirigami2_HEADERS + RELATIVE .. + ) +endif() + +install(TARGETS KF5Kirigami2 + EXPORT KF5Kirigami2Targets + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) + +if(BUILD_SHARED_LIBS) + list(APPEND Kirigami2_HEADERS ${CMAKE_CURRENT_BINARY_DIR}/kirigami2_export.h) +endif() + +install(FILES ${Kirigami2_HEADERS} + DESTINATION ${KIRIGAMI_INSTALL_INCLUDEDIR}/kirigami # prefix matching C++ namespace + COMPONENT Devel) +install(FILES ${Kirigami2_CamelCase_HEADERS} + DESTINATION ${KIRIGAMI_INSTALL_INCLUDEDIR}/Kirigami # prefix matching C++ namespace + COMPONENT Devel) + +# provide compat headers for old broken C++ namespaceless include path +if(NOT EXCLUDE_DEPRECATED_BEFORE_AND_AT STREQUAL "CURRENT" AND + EXCLUDE_DEPRECATED_BEFORE_AND_AT VERSION_LESS 5.91.0) + + function(generate_compat_headers) + foreach(classname ${ARGV}) + string(TOLOWER ${classname} classname_lc) + set(HEADER_NAME "${classname_lc}.h") + # normal header + set(compat_header "${CMAKE_CURRENT_BINARY_DIR}/compat/${HEADER_NAME}") + set(NEW_INCLUDE "kirigami/${HEADER_NAME}") + configure_file(compatheader.h.in ${compat_header} @ONLY) + install(FILES ${compat_header} + DESTINATION ${KIRIGAMI_INSTALL_INCLUDEDIR} + COMPONENT Devel + ) + # CamelCase header + set(compat_header "${CMAKE_CURRENT_BINARY_DIR}/compat/${classname}") + set(NEW_INCLUDE "Kirigami/${classname}") + configure_file(compatheader.h.in ${compat_header} @ONLY) + install(FILES ${compat_header} + DESTINATION ${KIRIGAMI_INSTALL_INCLUDEDIR} + COMPONENT Devel + ) + endforeach() + endfunction() + + generate_compat_headers( + PlatformTheme + KirigamiPluginFactory + TabletModeWatcher + Units + ) +endif() + +if(BUILD_QCH) + ecm_add_qch( + KF5Kirigami2_QCH + NAME Kirigami2 + BASE_NAME KF5Kirigami2 + VERSION ${KF_VERSION} + ORG_DOMAIN org.kde + SOURCES # using only public headers, to cover only public API + ${Kirigami2_HEADERS} + MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md" + LINK_QCHS + Qt5Core_QCH + BLANK_MACROS + KIRIGAMI_EXPORT + KIRIGAMI_DEPRECATED + TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} + QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} + COMPONENT Devel + ) +endif() + +include(ECMGeneratePriFile) +ecm_generate_pri_file(BASE_NAME Kirigami2 + LIB_NAME KF5Kirigami2 + DEPS "core" + FILENAME_VAR PRI_FILENAME + INCLUDE_INSTALL_DIR ${KIRIGAMI_INSTALL_INCLUDEDIR} +) +install(FILES ${PRI_FILENAME} + DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) + diff --git a/src/libkirigami/basictheme.cpp b/src/libkirigami/basictheme.cpp new file mode 100644 index 0000000..a96448e --- /dev/null +++ b/src/libkirigami/basictheme.cpp @@ -0,0 +1,314 @@ +/* + * SPDX-FileCopyrightText: 2017 by Marco Martin + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "basictheme_p.h" + +#include "styleselector_p.h" +#include +#include + +#include "loggingcategory.h" + +namespace Kirigami +{ +class CompatibilityThemeDefinition : public BasicThemeDefinition +{ + Q_OBJECT +public: + CompatibilityThemeDefinition(QObject *component, QObject *parent = nullptr) + : BasicThemeDefinition(parent) + { + m_object = component; + + connect(m_object, SIGNAL(textColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(disabledTextColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(highlightColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(highlightedTextColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(backgroundColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(alternateBackgroundColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(linkColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(visitedLinkColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(buttonTextColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(buttonBackgroundColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(buttonAlternateBackgroundColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(buttonHoverColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(buttonFocusColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(viewTextColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(viewBackgroundColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(viewAlternateBackgroundColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(viewHoverColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(viewFocusColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(complementaryTextColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(complementaryBackgroundColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(complementaryAlternateBackgroundColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(complementaryHoverColorChanged()), this, SLOT(syncFromQml())); + connect(m_object, SIGNAL(complementaryFocusColorChanged()), this, SLOT(syncFromQml())); + } + + void syncToQml(PlatformTheme *object) override + { + BasicThemeDefinition::syncToQml(object); + + QMetaObject::invokeMethod(m_object, "__propagateColorSet", Q_ARG(QVariant, QVariant::fromValue(object->parent())), Q_ARG(QVariant, object->colorSet())); + QMetaObject::invokeMethod(m_object, + "__propagateTextColor", + Q_ARG(QVariant, QVariant::fromValue(object->parent())), + Q_ARG(QVariant, object->textColor())); + QMetaObject::invokeMethod(m_object, + "__propagateBackgroundColor", + Q_ARG(QVariant, QVariant::fromValue(object->parent())), + Q_ARG(QVariant, object->backgroundColor())); + QMetaObject::invokeMethod(m_object, + "__propagatePrimaryColor", + Q_ARG(QVariant, QVariant::fromValue(object->parent())), + Q_ARG(QVariant, object->highlightColor())); + QMetaObject::invokeMethod(m_object, + "__propagateAccentColor", + Q_ARG(QVariant, QVariant::fromValue(object->parent())), + Q_ARG(QVariant, object->highlightColor())); + } + + Q_SLOT void syncFromQml() + { + textColor = m_object->property("textColor").value(); + disabledTextColor = m_object->property("disabledTextColor").value(); + highlightColor = m_object->property("highlightColor").value(); + highlightedTextColor = m_object->property("highlightedTextColor").value(); + backgroundColor = m_object->property("backgroundColor").value(); + alternateBackgroundColor = m_object->property("alternateBackgroundColor").value(); + linkColor = m_object->property("linkColor").value(); + visitedLinkColor = m_object->property("visitedLinkColor").value(); + buttonTextColor = m_object->property("buttonTextColor").value(); + buttonBackgroundColor = m_object->property("buttonBackgroundColor").value(); + buttonAlternateBackgroundColor = m_object->property("buttonAlternateBackgroundColor").value(); + buttonHoverColor = m_object->property("buttonHoverColor").value(); + buttonFocusColor = m_object->property("buttonFocusColor").value(); + viewTextColor = m_object->property("viewTextColor").value(); + viewBackgroundColor = m_object->property("viewBackgroundColor").value(); + viewAlternateBackgroundColor = m_object->property("viewAlternateBackgroundColor").value(); + viewHoverColor = m_object->property("viewHoverColor").value(); + viewFocusColor = m_object->property("viewFocusColor").value(); + complementaryTextColor = m_object->property("complementaryTextColor").value(); + complementaryBackgroundColor = m_object->property("complementaryBackgroundColor").value(); + complementaryAlternateBackgroundColor = m_object->property("complementaryAlternateBackgroundColor").value(); + complementaryHoverColor = m_object->property("complementaryHoverColor").value(); + complementaryFocusColor = m_object->property("complementaryFocusColor").value(); + + Q_EMIT changed(); + } + +private: + QObject *m_object; +}; + +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; + } + + auto componentUrl = StyleSelector::componentUrl(QStringLiteral("Theme.qml")); + QString path{componentUrl.toLocalFile()}; + if (path.isEmpty() && componentUrl.scheme() == QLatin1String("qrc")) { + path = QLatin1Char(':') + componentUrl.path(); + } + QFile themeFile{path}; + if (themeFile.open(QIODevice::ReadOnly)) { + auto data = themeFile.readAll(); + + // Before Kirigami 5.80, custom Theme files would be registered as a + // "Theme" singleton that would then be proxied by BasicTheme. This has + // changed with the Theme singleton being an instance of BasicTheme. + // However, this means that old theme files fail to load because + // QQmlComponent complains about "pragma Singleton". To workaround this, + // we remove the pragma here, as everything else should still work + // correctly. + // TODO KF6: Remove this and rely on all Theme files not being singletons. + data.replace("\npragma Singleton\n", ""); + + QQmlComponent component(engine); + component.setData(data, componentUrl); + auto result = component.create(); + + if (!result) { + const auto errors = component.errors(); + for (auto error : errors) { + qCWarning(KirigamiLog) << error.toString(); + } + + qCWarning(KirigamiLog) << "Invalid Theme file, using default Basic theme."; + m_themeDefinition = std::make_unique(); + } else if (qobject_cast(result)) { + m_themeDefinition.reset(qobject_cast(result)); + } else { + qCWarning(KirigamiLog) << "Warning: Theme implementations should use Kirigami.BasicThemeDefinition for its root item"; + m_themeDefinition = std::make_unique(result); + } + } else { + qCDebug(KirigamiLog) << "No Theme file found, 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() +{ + 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) +{ + 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 "basictheme.moc" diff --git a/src/libkirigami/basictheme_p.h b/src/libkirigami/basictheme_p.h new file mode 100644 index 0000000..60d1009 --- /dev/null +++ b/src/libkirigami/basictheme_p.h @@ -0,0 +1,193 @@ +/* + * SPDX-FileCopyrightText: 2017 by Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef BASICTHEME_H +#define BASICTHEME_H + +#include "platformtheme.h" + +#include "kirigami2_export.h" + +namespace Kirigami +{ +class BasicTheme; + +class KIRIGAMI2_EXPORT BasicThemeDefinition : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QColor textColor MEMBER textColor NOTIFY changed) + Q_PROPERTY(QColor disabledTextColor MEMBER disabledTextColor NOTIFY changed) + + Q_PROPERTY(QColor highlightColor MEMBER highlightColor NOTIFY changed) + Q_PROPERTY(QColor highlightedTextColor MEMBER highlightedTextColor NOTIFY changed) + Q_PROPERTY(QColor backgroundColor MEMBER backgroundColor NOTIFY changed) + Q_PROPERTY(QColor alternateBackgroundColor MEMBER alternateBackgroundColor NOTIFY changed) + + Q_PROPERTY(QColor focusColor MEMBER focusColor NOTIFY changed) + Q_PROPERTY(QColor hoverColor MEMBER hoverColor NOTIFY changed) + + Q_PROPERTY(QColor activeTextColor MEMBER activeTextColor NOTIFY changed) + Q_PROPERTY(QColor activeBackgroundColor MEMBER activeBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor linkColor MEMBER linkColor NOTIFY changed) + Q_PROPERTY(QColor linkBackgroundColor MEMBER linkBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor visitedLinkColor MEMBER visitedLinkColor NOTIFY changed) + Q_PROPERTY(QColor visitedLinkBackgroundColor MEMBER visitedLinkBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor negativeTextColor MEMBER negativeTextColor NOTIFY changed) + Q_PROPERTY(QColor negativeBackgroundColor MEMBER negativeBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor neutralTextColor MEMBER neutralTextColor NOTIFY changed) + Q_PROPERTY(QColor neutralBackgroundColor MEMBER neutralBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor positiveTextColor MEMBER positiveTextColor NOTIFY changed) + Q_PROPERTY(QColor positiveBackgroundColor MEMBER positiveBackgroundColor NOTIFY changed) + + Q_PROPERTY(QColor buttonTextColor MEMBER buttonTextColor NOTIFY changed) + Q_PROPERTY(QColor buttonBackgroundColor MEMBER buttonBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor buttonAlternateBackgroundColor MEMBER buttonAlternateBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor buttonHoverColor MEMBER buttonHoverColor NOTIFY changed) + Q_PROPERTY(QColor buttonFocusColor MEMBER buttonFocusColor NOTIFY changed) + + Q_PROPERTY(QColor viewTextColor MEMBER viewTextColor NOTIFY changed) + Q_PROPERTY(QColor viewBackgroundColor MEMBER viewBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor viewAlternateBackgroundColor MEMBER viewAlternateBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor viewHoverColor MEMBER viewHoverColor NOTIFY changed) + Q_PROPERTY(QColor viewFocusColor MEMBER viewFocusColor NOTIFY changed) + + Q_PROPERTY(QColor selectionTextColor MEMBER selectionTextColor NOTIFY changed) + Q_PROPERTY(QColor selectionBackgroundColor MEMBER selectionBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor selectionAlternateBackgroundColor MEMBER selectionAlternateBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor selectionHoverColor MEMBER selectionHoverColor NOTIFY changed) + Q_PROPERTY(QColor selectionFocusColor MEMBER selectionFocusColor NOTIFY changed) + + Q_PROPERTY(QColor tooltipTextColor MEMBER tooltipTextColor NOTIFY changed) + Q_PROPERTY(QColor tooltipBackgroundColor MEMBER tooltipBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor tooltipAlternateBackgroundColor MEMBER tooltipAlternateBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor tooltipHoverColor MEMBER tooltipHoverColor NOTIFY changed) + Q_PROPERTY(QColor tooltipFocusColor MEMBER tooltipFocusColor NOTIFY changed) + + Q_PROPERTY(QColor complementaryTextColor MEMBER complementaryTextColor NOTIFY changed) + Q_PROPERTY(QColor complementaryBackgroundColor MEMBER complementaryBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor complementaryAlternateBackgroundColor MEMBER complementaryAlternateBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor complementaryHoverColor MEMBER complementaryHoverColor NOTIFY changed) + Q_PROPERTY(QColor complementaryFocusColor MEMBER complementaryFocusColor NOTIFY changed) + + Q_PROPERTY(QColor headerTextColor MEMBER headerTextColor NOTIFY changed) + Q_PROPERTY(QColor headerBackgroundColor MEMBER headerBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor headerAlternateBackgroundColor MEMBER headerAlternateBackgroundColor NOTIFY changed) + Q_PROPERTY(QColor headerHoverColor MEMBER headerHoverColor NOTIFY changed) + Q_PROPERTY(QColor headerFocusColor MEMBER headerFocusColor NOTIFY changed) + + Q_PROPERTY(QFont defaultFont MEMBER defaultFont NOTIFY changed) + Q_PROPERTY(QFont smallFont MEMBER smallFont NOTIFY changed) + +public: + explicit BasicThemeDefinition(QObject *parent = nullptr); + + virtual void syncToQml(PlatformTheme *object); + + QColor textColor = QColor{"#31363b"}; + QColor disabledTextColor = QColor{"#9931363b"}; + + QColor highlightColor = QColor{"#2196F3"}; + QColor highlightedTextColor = QColor{"#eff0fa"}; + QColor backgroundColor = QColor{"#eff0f1"}; + QColor alternateBackgroundColor = QColor{"#bdc3c7"}; + + QColor focusColor = QColor{"#2196F3"}; + QColor hoverColor = QColor{"#2196F3"}; + + QColor activeTextColor = QColor{"#0176D3"}; + QColor activeBackgroundColor = QColor{"#0176D3"}; + QColor linkColor = QColor{"#2196F3"}; + QColor linkBackgroundColor = QColor{"#2196F3"}; + QColor visitedLinkColor = QColor{"#2196F3"}; + QColor visitedLinkBackgroundColor = QColor{"#2196F3"}; + QColor negativeTextColor = QColor{"#DA4453"}; + QColor negativeBackgroundColor = QColor{"#DA4453"}; + QColor neutralTextColor = QColor{"#F67400"}; + QColor neutralBackgroundColor = QColor{"#F67400"}; + QColor positiveTextColor = QColor{"#27AE60"}; + QColor positiveBackgroundColor = QColor{"#27AE60"}; + + QColor buttonTextColor = QColor{"#31363b"}; + QColor buttonBackgroundColor = QColor{"#eff0f1"}; + QColor buttonAlternateBackgroundColor = QColor{"#bdc3c7"}; + QColor buttonHoverColor = QColor{"#2196F3"}; + QColor buttonFocusColor = QColor{"#2196F3"}; + + QColor viewTextColor = QColor{"#31363b"}; + QColor viewBackgroundColor = QColor{"#fcfcfc"}; + QColor viewAlternateBackgroundColor = QColor{"#eff0f1"}; + QColor viewHoverColor = QColor{"#2196F3"}; + QColor viewFocusColor = QColor{"#2196F3"}; + + QColor selectionTextColor = QColor{"#eff0fa"}; + QColor selectionBackgroundColor = QColor{"#2196F3"}; + QColor selectionAlternateBackgroundColor = QColor{"#1d99f3"}; + QColor selectionHoverColor = QColor{"#2196F3"}; + QColor selectionFocusColor = QColor{"#2196F3"}; + + QColor tooltipTextColor = QColor{"#eff0f1"}; + QColor tooltipBackgroundColor = QColor{"#31363b"}; + QColor tooltipAlternateBackgroundColor = QColor{"#4d4d4d"}; + QColor tooltipHoverColor = QColor{"#2196F3"}; + QColor tooltipFocusColor = QColor{"#2196F3"}; + + QColor complementaryTextColor = QColor{"#eff0f1"}; + QColor complementaryBackgroundColor = QColor{"#31363b"}; + QColor complementaryAlternateBackgroundColor = QColor{"#3b4045"}; + QColor complementaryHoverColor = QColor{"#2196F3"}; + QColor complementaryFocusColor = QColor{"#2196F3"}; + + QColor headerTextColor = QColor{"#232629"}; + QColor headerBackgroundColor = QColor{"#e3e5e7"}; + QColor headerAlternateBackgroundColor = QColor{"#eff0f1"}; + QColor headerHoverColor = QColor{"#2196F3"}; + QColor headerFocusColor = QColor{"#93cee9"}; + + 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); + + QVector 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/libkirigami/compatheader.h.in b/src/libkirigami/compatheader.h.in new file mode 100644 index 0000000..de61efb --- /dev/null +++ b/src/libkirigami/compatheader.h.in @@ -0,0 +1,10 @@ +#include + +#if KIRIGAMI2_ENABLE_DEPRECATED_SINCE(5, 91) +# include +# if KIRIGAMI2_DEPRECATED_WARNINGS_SINCE >= 0x055b00 +# pragma message("Deprecated header. Since 5.91, use #include <@NEW_INCLUDE@> instead") +# endif +#else +# error "Include of deprecated header is disabled" +#endif diff --git a/src/libkirigami/kirigamipluginfactory.cpp b/src/libkirigami/kirigamipluginfactory.cpp new file mode 100644 index 0000000..3bcd410 --- /dev/null +++ b/src/libkirigami/kirigamipluginfactory.cpp @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: 2017 by Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "kirigamipluginfactory.h" + +#include + +#include +#include +#include + +#include "styleselector_p.h" +#include "units.h" + +#include "loggingcategory.h" + +namespace Kirigami { + +KirigamiPluginFactory::KirigamiPluginFactory(QObject *parent) + : QObject(parent) +{ +} + +KirigamiPluginFactory::~KirigamiPluginFactory() = default; + +KirigamiPluginFactory *KirigamiPluginFactory::findPlugin() +{ + static KirigamiPluginFactory *pluginFactory = nullptr; + + //check for the plugin only once: it's an heavy operation + if (pluginFactory) { + return pluginFactory; + } + + static bool s_factoryChecked = false; + + if (!s_factoryChecked) { + s_factoryChecked = true; + + #ifdef KIRIGAMI_BUILD_TYPE_STATIC + for (QObject *staticPlugin : QPluginLoader::staticInstances()) { + KirigamiPluginFactory *factory = qobject_cast(staticPlugin); + if (factory) { + pluginFactory = factory; + } + } + #else + const auto libraryPaths = QCoreApplication::libraryPaths(); + for (const QString &path : libraryPaths) { + #ifdef Q_OS_ANDROID + QDir dir(path); + #else + QDir dir(path + QStringLiteral("/kf" QT_STRINGIFY(QT_VERSION_MAJOR) "/kirigami")); + #endif + const auto fileNames = dir.entryList(QDir::Files); + + for (const QString &fileName : fileNames) { + +#ifdef Q_OS_ANDROID + if (fileName.startsWith(QStringLiteral("libplugins_kf" QT_STRINGIFY(QT_VERSION_MAJOR) "_kirigami_")) && QLibrary::isLibrary(fileName)) { +#endif + // TODO: env variable? + if (!QQuickStyle::name().isEmpty() && fileName.contains(QQuickStyle::name())) { + QPluginLoader loader(dir.absoluteFilePath(fileName)); + QObject *plugin = loader.instance(); + // TODO: load actually a factory as plugin + + qCDebug(KirigamiLog) << "Loading style plugin from" << dir.absoluteFilePath(fileName); + + KirigamiPluginFactory *factory = qobject_cast(plugin); + if (factory) { + pluginFactory = 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 (pluginFactory) { + break; + } + } +#endif + } + + return pluginFactory; +} + +KirigamiPluginFactoryV2::KirigamiPluginFactoryV2(QObject *parent) + : KirigamiPluginFactory(parent) +{ +} + +KirigamiPluginFactoryV2::~KirigamiPluginFactoryV2() = default; +} + +#include "moc_kirigamipluginfactory.cpp" diff --git a/src/libkirigami/kirigamipluginfactory.h b/src/libkirigami/kirigamipluginfactory.h new file mode 100644 index 0000000..0f96111 --- /dev/null +++ b/src/libkirigami/kirigamipluginfactory.h @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2017 by Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef KIRIGAMIPLUGINFACTORY_H +#define KIRIGAMIPLUGINFACTORY_H + +#include "platformtheme.h" +#include + +#ifndef KIRIGAMI_BUILD_TYPE_STATIC +#include "kirigami2_export.h" +#endif + +class QQmlEngine; + +namespace Kirigami { +class Units; + +/** + * @class KirigamiPluginFactory kirigamipluginfactory.h + * + * This class is reimpleented by plugins to provide different implementations + * of PlatformTheme + */ +#ifdef KIRIGAMI_BUILD_TYPE_STATIC +class KirigamiPluginFactory : public QObject +#else +class KIRIGAMI2_EXPORT KirigamiPluginFactory : public QObject +#endif +{ + Q_OBJECT + +public: + explicit KirigamiPluginFactory(QObject *parent = nullptr); + ~KirigamiPluginFactory() 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; + + /** + * 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 + * @return pointer to the KirigamiPluginFactory of the current style + */ + static KirigamiPluginFactory *findPlugin(); +}; + +// TODO KF6 unify KirigamiPluginFactory and KirigamiPluginFactoryV2 again +/** + * This class provides an extended version of KirigamiPluginFactory. + * Plugins that support Units need to implement it instead of KirigamiPluginFactory. + */ +#ifdef KIRIGAMI_BUILD_TYPE_STATIC +class KirigamiPluginFactoryV2 : public KirigamiPluginFactory +#else +class KIRIGAMI2_EXPORT KirigamiPluginFactoryV2 : public KirigamiPluginFactory +#endif +{ + Q_OBJECT + +public: + explicit KirigamiPluginFactoryV2(QObject *parent = nullptr); + ~KirigamiPluginFactoryV2() override; + + /** + * 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; +}; +} + +QT_BEGIN_NAMESPACE +#define KirigamiPluginFactory_iid "org.kde.kirigami.KirigamiPluginFactory" +Q_DECLARE_INTERFACE(Kirigami::KirigamiPluginFactory, KirigamiPluginFactory_iid) +QT_END_NAMESPACE + +#endif // KIRIGAMIPLUGINFACTORY_H diff --git a/src/libkirigami/org.kde.KWin.TabletModeManager.xml b/src/libkirigami/org.kde.KWin.TabletModeManager.xml new file mode 100644 index 0000000..1daf5f0 --- /dev/null +++ b/src/libkirigami/org.kde.KWin.TabletModeManager.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/libkirigami/org.kde.KWin.VirtualKeyboard.xml b/src/libkirigami/org.kde.KWin.VirtualKeyboard.xml new file mode 100644 index 0000000..e0a6650 --- /dev/null +++ b/src/libkirigami/org.kde.KWin.VirtualKeyboard.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libkirigami/platformtheme.cpp b/src/libkirigami/platformtheme.cpp new file mode 100644 index 0000000..a087ddc --- /dev/null +++ b/src/libkirigami/platformtheme.cpp @@ -0,0 +1,1006 @@ +/* + * SPDX-FileCopyrightText: 2017 by Marco Martin + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "platformtheme.h" +#include "basictheme_p.h" +#include "kirigamipluginfactory.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Kirigami +{ +template<> +KIRIGAMI2_EXPORT QEvent::Type PlatformThemeEvents::DataChangedEvent::type = QEvent::None; +template<> +KIRIGAMI2_EXPORT QEvent::Type PlatformThemeEvents::ColorSetChangedEvent::type = QEvent::None; +template<> +KIRIGAMI2_EXPORT QEvent::Type PlatformThemeEvents::ColorGroupChangedEvent::type = QEvent::None; +template<> +KIRIGAMI2_EXPORT QEvent::Type PlatformThemeEvents::ColorChangedEvent::type = QEvent::None; +template<> +KIRIGAMI2_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 *; + QVector 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); + 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) + , 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()) { + 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); + } + + emitCompressedColorChanged(theme); + } + + return; + } + + auto itr = localOverrides->find(color); + if (itr != localOverrides->end() && itr->second == value && (data && data->owner != theme)) { + return; + } + + (*localOverrides)[color] = value; + + if (data) { + data->setColor(theme, color, value); + } + + emitCompressedColorChanged(theme); + } + + 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; + } + } + + if (data) { + data->setColor(theme, color, value); + } + } + + inline void emitCompressedColorChanged(PlatformTheme *theme) + { + if (pendingColorChange) { + return; + } + + pendingColorChange = true; + QMetaObject::invokeMethod(theme, &PlatformTheme::emitColorChanged, Qt::QueuedConnection); + } + + inline void queueChildUpdate(PlatformTheme *theme) + { + if (pendingChildUpdate) { + return; + } + + pendingChildUpdate = true; + QMetaObject::invokeMethod( + theme, + [this, theme]() { + pendingChildUpdate = false; + theme->updateChildren(theme->parent()); + }, + Qt::QueuedConnection); + } + + /* + * 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; + + // 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"); + + static KirigamiPluginFactory *s_pluginFactory; +}; + +KirigamiPluginFactory *PlatformThemePrivate::s_pluginFactory = nullptr; + +PlatformTheme::PlatformTheme(QObject *parent) + : QObject(parent) + , d(new PlatformThemePrivate) +{ + if (QQuickItem *item = qobject_cast(parent)) { + connect(item, &QQuickItem::windowChanged, this, &PlatformTheme::update); + connect(item, &QQuickItem::parentChanged, this, &PlatformTheme::update); + } + + update(); +} + +PlatformTheme::~PlatformTheme() +{ + if (d->data) { + d->data->removeChangeWatcher(this); + } + + delete d; +} + +void PlatformTheme::setColorSet(PlatformTheme::ColorSet 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) +{ + 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) +{ + 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) +{ + if (d->data) { + d->data->setSmallFont(this, font); + } +} + +// 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); +} + +QPalette PlatformTheme::palette() const +{ + if (!d->data) { + return QPalette{}; + } + + auto palette = d->data->palette; + + if (d->localOverrides) { + PlatformThemeData::updatePalette(palette, *d->localOverrides); + } + + return palette; +} + +#if KIRIGAMI2_BUILD_DEPRECATED_SINCE(5, 80) +void PlatformTheme::setPalette(const QPalette &palette) +{ + Q_UNUSED(palette); +} +#endif + +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) +{ + auto plugin = KirigamiPluginFactory::findPlugin(); + if (plugin) { + if (auto theme = plugin->createPlatformTheme(object)) { + return theme; + } + } + + return new BasicTheme(object); +} + +bool PlatformTheme::event(QEvent *event) +{ + 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); + + Q_EMIT colorSetChanged(data->colorSet); + Q_EMIT colorGroupChanged(data->colorGroup); + Q_EMIT defaultFontChanged(data->defaultFont); + Q_EMIT smallFontChanged(data->smallFont); + d->emitCompressedColorChanged(this); + } + + return true; + } + + if (event->type() == PlatformThemeEvents::ColorSetChangedEvent::type) { + if (d->data) { + Q_EMIT colorSetChanged(d->data->colorSet); + } + return true; + } + + if (event->type() == PlatformThemeEvents::ColorGroupChangedEvent::type) { + if (d->data) { + Q_EMIT colorGroupChanged(d->data->colorGroup); + } + return true; + } + + if (event->type() == PlatformThemeEvents::ColorChangedEvent::type) { + d->emitCompressedColorChanged(this); + return true; + } + + if (event->type() == PlatformThemeEvents::FontChangedEvent::type) { + if (d->data) { + Q_EMIT defaultFontChanged(d->data->defaultFont); + Q_EMIT smallFontChanged(d->data->smallFont); + } + return true; + } + + return QObject::event(event); +} + +void PlatformTheme::update() +{ + d->queueChildUpdate(this); + + auto oldData = d->data; + + if (d->inherit) { + 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->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 (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); + } + } +} + +void PlatformTheme::emitColorChanged() +{ + if (d->data) { + Q_EMIT paletteChanged(d->data->palette); + } + + Q_EMIT colorsChanged(); + d->pendingColorChange = false; +} + +// 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(); + } +} + +} + +#include "platformtheme.moc" diff --git a/src/libkirigami/platformtheme.h b/src/libkirigami/platformtheme.h new file mode 100644 index 0000000..fa70595 --- /dev/null +++ b/src/libkirigami/platformtheme.h @@ -0,0 +1,385 @@ +/* + * SPDX-FileCopyrightText: 2017 by Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef PLATFORMTHEME_H +#define PLATFORMTHEME_H + +#include +#include +#include +#include +#include + +#include "kirigami2_export.h" + +namespace Kirigami +{ +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 KIRIGAMI2_EXPORT PlatformTheme : public QObject +{ + Q_OBJECT + + /** + * 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) + + /** + * This enumeration describes the color group used to generate the colors. + * The enum value is based upon QPalette::CpolorGroup 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) + + /** + * 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) + + // 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) + + /** + * Foreground color for disabled areas, usually a mid-gray + */ + Q_PROPERTY(QColor disabledTextColor READ disabledTextColor WRITE setCustomDisabledTextColor RESET setCustomDisabledTextColor NOTIFY colorsChanged) + + /** + * 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) + + /** + * Color for links + */ + Q_PROPERTY(QColor linkColor READ linkColor WRITE setCustomLinkColor RESET setCustomLinkColor NOTIFY colorsChanged) + + /** + * Color for visited links, usually a bit darker than linkColor + */ + Q_PROPERTY(QColor visitedLinkColor READ visitedLinkColor WRITE setCustomVisitedLinkColor RESET setCustomVisitedLinkColor NOTIFY colorsChanged) + + /** + * Foreground color for negative areas, such as critical error text + */ + Q_PROPERTY(QColor negativeTextColor READ negativeTextColor WRITE setCustomNegativeTextColor RESET setCustomNegativeTextColor NOTIFY colorsChanged) + + /** + * Foreground color for neutral areas, such as warning texts (but not critical) + */ + Q_PROPERTY(QColor neutralTextColor READ neutralTextColor WRITE setCustomNeutralTextColor RESET setCustomNeutralTextColor NOTIFY colorsChanged) + + /** + * Success messages, trusted content + */ + Q_PROPERTY(QColor positiveTextColor READ positiveTextColor WRITE setCustomPositiveTextColor RESET setCustomPositiveTextColor NOTIFY colorsChanged) + + // background colors + /** + * The generic background color + */ + Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setCustomBackgroundColor RESET setCustomBackgroundColor NOTIFY colorsChanged) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * A decoration color that indicates mouse hovering + */ + Q_PROPERTY(QColor hoverColor READ hoverColor WRITE setCustomHoverColor RESET setCustomHoverColor NOTIFY colorsChanged) + + // font and palette + Q_PROPERTY(QFont defaultFont READ defaultFont NOTIFY defaultFontChanged) + + // small font + Q_PROPERTY(QFont smallFont READ smallFont NOTIFY defaultFontChanged) + + // Active palette + Q_PROPERTY(QPalette palette READ palette NOTIFY paletteChanged) + +public: + enum ColorSet { + View = 0, /** Color set for item views, usually the lightest of all */ + Window, /** Default Color set for windows and "chrome" areas */ + Button, /** Color set used by buttons */ + Selection, /** Color set used by selectged areas */ + Tooltip, /** Color set used by tooltips */ + Complementary, /** Color set meant to be complementary to Window: usually is a dark theme for light themes */ + Header, /** Color set to be used by heading areas of applications, such as toolbars */ + + ColorSetCount, // Number of items in this enum, this should always be the last item. + }; + 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; + + // 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()); + + // QML attached property + static PlatformTheme *qmlAttachedProperties(QObject *object); + +Q_SIGNALS: + // TODO: parameters to signals as this is also a c++ api + void colorsChanged(); + void defaultFontChanged(const QFont &font); + void smallFontChanged(const QFont &font); + void colorSetChanged(Kirigami::PlatformTheme::ColorSet colorSet); + void colorGroupChanged(Kirigami::PlatformTheme::ColorGroup colorGroup); + void paletteChanged(const QPalette &pal); + void inheritChanged(bool inherit); + +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); + +#if KIRIGAMI2_ENABLE_DEPRECATED_SINCE(5, 80) + KIRIGAMI2_DEPRECATED_VERSION(5, 80, "setPalette is unused, the palette is autogenerated from PlatformTheme colors now") + void setPalette(const QPalette &palette); +#endif + + bool event(QEvent *event) override; + +private: + void update(); + void updateChildren(QObject *item); + void emitSignals(); + void emitColorChanged(); + QObject *determineParent(QObject *object); + + PlatformThemePrivate *d; + friend class PlatformThemePrivate; + friend class PlatformThemeData; +}; + +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 KIRIGAMI2_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 + +QML_DECLARE_TYPEINFO(Kirigami::PlatformTheme, QML_HAS_ATTACHED_PROPERTIES) + +#endif // PLATFORMTHEME_H diff --git a/src/libkirigami/styleselector.cpp b/src/libkirigami/styleselector.cpp new file mode 100644 index 0000000..51f1131 --- /dev/null +++ b/src/libkirigami/styleselector.cpp @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "styleselector_p.h" + +#include +#include +#include + +namespace Kirigami +{ +QUrl StyleSelector::s_baseUrl; +QStringList StyleSelector::s_styleChain; + +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)); + } + } + + 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) + return QStringLiteral(":/org/kde/kirigami.2/") + path; +#elif defined(Q_OS_ANDROID) + return QStringLiteral(":/android_rcc_bundle/qml/org/kde/kirigami.2/") + 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) + return QStringLiteral("qrc:/org/kde/kirigami.2/") + path; +#elif defined(Q_OS_ANDROID) + return QStringLiteral("qrc:/android_rcc_bundle/qml/org/kde/kirigami.2/") + path; +#else + return s_baseUrl.toString() + QLatin1Char('/') + path; +#endif +} + +} diff --git a/src/libkirigami/styleselector_p.h b/src/libkirigami/styleselector_p.h new file mode 100644 index 0000000..80c5581 --- /dev/null +++ b/src/libkirigami/styleselector_p.h @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef STYLESELECTOR_H +#define STYLESELECTOR_H + +#include + +#include "kirigami2_export.h" + +class QUrl; + +namespace Kirigami +{ +class KIRIGAMI2_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: + static QUrl s_baseUrl; + static QStringList s_styleChain; +}; + +} + +#endif // STYLESELECTOR_H diff --git a/src/libkirigami/tabletmodewatcher.cpp b/src/libkirigami/tabletmodewatcher.cpp new file mode 100644 index 0000000..3286c9d --- /dev/null +++ b/src/libkirigami/tabletmodewatcher.cpp @@ -0,0 +1,142 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "tabletmodewatcher.h" +#include + +#if defined(KIRIGAMI_ENABLE_DBUS) +#include "tabletmodemanager_interface.h" +#include +#endif + +// TODO: All the dbus stuff should be conditional, optional win32 support + +namespace Kirigami +{ +KIRIGAMI2_EXPORT QEvent::Type TabletModeChangedEvent::type = QEvent::None; + +class TabletModeWatcherSingleton +{ +public: + TabletModeWatcher self; +}; + +Q_GLOBAL_STATIC(TabletModeWatcherSingleton, privateTabletModeWatcherSelf) + +class TabletModeWatcherPrivate +{ +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")) { + /* clang-format off */ + isTabletMode = + (QString::fromLatin1(qgetenv("QT_QUICK_CONTROLS_MOBILE")) == QStringLiteral("1") + || QString::fromLatin1(qgetenv("QT_QUICK_CONTROLS_MOBILE")) == QStringLiteral("true")) + || (QString::fromLatin1(qgetenv("KDE_KIRIGAMI_TABLET_MODE")) == QStringLiteral("1") + || QString::fromLatin1(qgetenv("KDE_KIRIGAMI_TABLET_MODE")) == QStringLiteral("true")); + /* clang-format on */ + isTabletModeAvailable = isTabletMode; + } else { + m_interface = + new OrgKdeKWinTabletModeManagerInterface(QStringLiteral("org.kde.KWin"), QStringLiteral("/org/kde/KWin"), QDBusConnection::sessionBus(), q); + + if (m_interface->isValid()) { + // NOTE: the initial call is actually sync, because is better a tiny freeze than having the ui always recalculated and changed at the start + isTabletModeAvailable = m_interface->tabletModeAvailable(); + isTabletMode = m_interface->tabletMode(); + QObject::connect(m_interface, &OrgKdeKWinTabletModeManagerInterface::tabletModeChanged, q, [this](bool tabletMode) { + setIsTablet(tabletMode); + }); + QObject::connect(m_interface, &OrgKdeKWinTabletModeManagerInterface::tabletModeAvailableChanged, q, [this](bool avail) { + isTabletModeAvailable = avail; + Q_EMIT q->tabletModeAvailableChanged(avail); + }); + } else { + isTabletModeAvailable = false; + isTabletMode = false; + } + } +// TODO: case for Windows +#else + isTabletModeAvailable = false; + isTabletMode = false; +#endif + } + ~TabletModeWatcherPrivate(){}; + void setIsTablet(bool tablet); + + TabletModeWatcher *q; +#if defined(KIRIGAMI_ENABLE_DBUS) + OrgKdeKWinTabletModeManagerInterface *m_interface = nullptr; +#endif + QVector 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/libkirigami/tabletmodewatcher.h b/src/libkirigami/tabletmodewatcher.h new file mode 100644 index 0000000..4768e94 --- /dev/null +++ b/src/libkirigami/tabletmodewatcher.h @@ -0,0 +1,99 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef KIRIGAMI_TABLETMODEWATCHER +#define KIRIGAMI_TABLETMODEWATCHER + +#include +#include + +#include "kirigami2_export.h" + +namespace Kirigami +{ +class TabletModeWatcherPrivate; + +class KIRIGAMI2_EXPORT TabletModeChangedEvent : public QEvent +{ +public: + TabletModeChangedEvent(bool tablet) + : QEvent(TabletModeChangedEvent::type) + , tabletMode(tablet) + { + } + + bool tabletMode = false; + + static QEvent::Type type; +}; + +/** + * @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. + */ +#ifdef KIRIGAMI_BUILD_TYPE_STATIC +class TabletModeWatcher : public QObject +#else +class KIRIGAMI2_EXPORT TabletModeWatcher : public QObject +#endif +{ + Q_OBJECT + Q_PROPERTY(bool tabletModeAvailable READ isTabletModeAvailable NOTIFY tabletModeAvailableChanged) + Q_PROPERTY(bool tabletMode READ isTabletMode NOTIFY tabletModeChanged) + +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: + TabletModeWatcher(QObject *parent = nullptr); + TabletModeWatcherPrivate *d; + friend class TabletModeWatcherSingleton; +}; +} + +#endif // KIRIGAMI_TABLETMODEWATCHER diff --git a/src/libkirigami/units.cpp b/src/libkirigami/units.cpp new file mode 100644 index 0000000..4d36f02 --- /dev/null +++ b/src/libkirigami/units.cpp @@ -0,0 +1,438 @@ +/* + * 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 "loggingcategory.h" + +namespace Kirigami { +using clock = std::chrono::steady_clock; + +const clock::duration rateLimit = std::chrono::seconds(1); + +/* Print a deprecation warning that is rate limited to only display once in + * every time period as determined by rateLimit. We keep track of how often this + * is called and display that if it is larger than 0. + * + * This is done to prevent flooding the logs with "X is deprecated" messages + * that are all the same and don't provide any new information after the first. + */ +void rateLimitWarning(const char *method, const char *since, const char *message) +{ + static QMap> messages; + + auto methodString = QString::fromUtf8(method); + + if (!messages.contains(methodString)) { + messages.insert(methodString, qMakePair(clock::time_point{}, 0)); + } + + auto entry = messages.value(methodString); + if (clock::now() - entry.first < rateLimit) { + messages[methodString].second += 1; + return; + } + + qCWarning(KirigamiLog).nospace() << method << " is deprecated (since " << since << "): " << message; + + if (entry.second > 0) { + qCWarning(KirigamiLog) << "Previous message repeats" << entry.second << "times."; + } + + messages[methodString] = qMakePair(clock::now(), 0); +} + +class UnitsPrivate +{ + Q_DISABLE_COPY(UnitsPrivate) + +public: + explicit UnitsPrivate(Units *units) +#if KIRIGAMI2_BUILD_DEPRECATED_SINCE(5, 86) + : qmlFontMetrics(nullptr) +#endif + // Cache font so we don't have to go through QVariant and property every time + , fontMetrics(QFontMetricsF(QGuiApplication::font())) + , gridUnit(fontMetrics.height()) + , smallSpacing(std::floor(gridUnit / 4)) + , mediumSpacing(std::round(smallSpacing * 1.5)) + , largeSpacing(smallSpacing * 2) + , veryLongDuration(400) + , longDuration(200) + , shortDuration(100) + , veryShortDuration(50) + , humanMoment(2000) + , toolTipDelay(700) +#if KIRIGAMI2_BUILD_DEPRECATED_SINCE(5, 86) + , wheelScrollLines(QGuiApplication::styleHints()->wheelScrollLines()) +#endif + , iconSizes(new IconSizes(units)) + { + } + + // Only stored for QML API compatiblity + // TODO KF6 drop +#if KIRIGAMI2_BUILD_DEPRECATED_SINCE(5, 86) + std::unique_ptr qmlFontMetrics = nullptr; +#endif + + // 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; + +#if KIRIGAMI2_BUILD_DEPRECATED_SINCE(5, 86) + int wheelScrollLines; +#endif + + IconSizes *const iconSizes; + + // To prevent overriding custom set units if the font changes + bool customUnitsSet = false; + bool customWheelScrollLinesSet = false; + +#if KIRIGAMI2_BUILD_DEPRECATED_SINCE(5, 86) + std::unique_ptr createQmlFontMetrics(QQmlEngine *engine) + { + QQmlComponent component(engine); + component.setData(QByteArrayLiteral( + "import QtQuick 2.14\n" + "import org.kde.kirigami 2.0\n" + "FontMetrics {\n" + " function roundedIconSize(size) {\n" + R"( console.warn("Units.fontMetrics.roundedIconSize is deprecated, use Units.iconSizes.roundedIconSize instead.");)" + " return Units.iconSizes.roundedIconSize(size)\n" + " }\n" + "}\n" + ), QUrl(QStringLiteral("units.cpp"))); + + return std::unique_ptr(component.create()); + } +#endif +}; + +Units::~Units() = default; + +Units::Units(QObject *parent) + : QObject(parent) + , d(std::make_unique(this)) +{ + connect(QGuiApplication::styleHints(), &QStyleHints::wheelScrollLinesChanged, this, [this](int scrollLines) { + if (d->customWheelScrollLinesSet) { + return; + } + + setWheelScrollLines(scrollLines); + }); + connect(qGuiApp, &QGuiApplication::fontChanged, this, [this](const QFont &font) { + d->fontMetrics = QFontMetricsF(font); + + if (d->customUnitsSet) { + return; + } + + d->gridUnit = d->fontMetrics.height(); + Q_EMIT gridUnitChanged(); + d->smallSpacing = std::floor(d->gridUnit / 4); + Q_EMIT smallSpacingChanged(); + d->mediumSpacing = std::round(d->smallSpacing * 1.5); + Q_EMIT mediumSpacingChanged(); + d->largeSpacing = d->smallSpacing * 2; + Q_EMIT largeSpacingChanged(); + Q_EMIT d->iconSizes->sizeForLabelsChanged(); + }); +} + +qreal Units::devicePixelRatio() const +{ + rateLimitWarning("Units.devicePixelRatio", "5.86", "This returns 1 when using Qt HiDPI scaling."); + const int pixelSize = QGuiApplication::font().pixelSize(); + const qreal pointSize = QGuiApplication::font().pointSize(); + + return std::fmax(1, (pixelSize * 0.75 / pointSize)); +} + +int Units::gridUnit() const +{ + return d->gridUnit; +} + +void Kirigami::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 Kirigami::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 Kirigami::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 Kirigami::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(); +} + +#if KIRIGAMI2_BUILD_DEPRECATED_SINCE(5, 86) +int Units::wheelScrollLines() const +{ + rateLimitWarning("Units.wheelScrollLines", "5.86", "Use Qt.styleHints.wheelScrollLines instead"); + return d->wheelScrollLines; +} + +void Units::setWheelScrollLines(int lines) +{ + if (d->wheelScrollLines == lines) { + return; + } + + d->wheelScrollLines = lines; + d->customWheelScrollLinesSet = true; + Q_EMIT wheelScrollLinesChanged(); +} +#endif + +IconSizes *Units::iconSizes() const +{ + return d->iconSizes; +} + +#if KIRIGAMI2_BUILD_DEPRECATED_SINCE(5, 86) +QObject *Units::fontMetrics() const +{ + rateLimitWarning("Units.fontMetrics", "5.86", "Create your own FontMetrics object instead."); + if (!d->qmlFontMetrics) { + d->qmlFontMetrics = d->createQmlFontMetrics(qmlEngine(this)); + } + return d->qmlFontMetrics.get(); +} +#endif + +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; +} + +} diff --git a/src/libkirigami/units.h b/src/libkirigami/units.h new file mode 100644 index 0000000..d8c757b --- /dev/null +++ b/src/libkirigami/units.h @@ -0,0 +1,291 @@ +/* + * SPDX-FileCopyrightText: 2021 Jonah Brüchert + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include + +#include "kirigami2_export.h" + +class QQmlEngine; + +namespace Kirigami { +class Units; +class UnitsPrivate; + +/** + * @class IconSizes units.h + * + * Provides access to platform-dependent icon sizing + */ +class KIRIGAMI2_EXPORT IconSizes : public QObject +{ + Q_OBJECT + + Q_PROPERTY(int sizeForLabels READ sizeForLabels NOTIFY sizeForLabelsChanged) + Q_PROPERTY(int small READ small NOTIFY smallChanged) + Q_PROPERTY(int smallMedium READ smallMedium NOTIFY smallMediumChanged) + Q_PROPERTY(int medium READ medium NOTIFY mediumChanged) + Q_PROPERTY(int large READ large NOTIFY largeChanged) + Q_PROPERTY(int huge READ huge NOTIFY hugeChanged) + Q_PROPERTY(int enormous READ enormous NOTIFY enormousChanged) + +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: + 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 KIRIGAMI2_EXPORT Units : public QObject +{ + Q_OBJECT + + friend class IconSizes; + + /** + * The fundamental unit of space that should be used for sizes, expressed in pixels. + * Given the screen has an accurate DPI settings, it corresponds to the height of + * the font's boundingRect. + */ + Q_PROPERTY(int gridUnit READ gridUnit WRITE setGridUnit NOTIFY gridUnitChanged) + + /** + * 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(IconSizes *iconSizes READ iconSizes CONSTANT) + + /** + * 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. + * Internally, this size depends on the size of the default font as rendered on the screen, + * so it takes user-configured font size and DPI into account. + */ + Q_PROPERTY(int smallSpacing READ smallSpacing WRITE setSmallSpacing NOTIFY smallSpacingChanged) + + /** + * This property holds the amount of spacing that should be used between medium UI elements, + * such as buttons and text fields in a toolbar. + * Internally, this size depends on the size of the default font as rendered on the screen, + * so it takes user-configured font size and DPI into account. + */ + Q_PROPERTY(int mediumSpacing READ mediumSpacing WRITE setMediumSpacing NOTIFY mediumSpacingChanged) + + /** + * 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. + * Internally, this size depends on the size of the default font as rendered on the screen, + * so it takes user-configured font size and DPI into account. + */ + Q_PROPERTY(int largeSpacing READ largeSpacing WRITE setLargeSpacing NOTIFY largeSpacingChanged) + + /** + * The ratio between physical and device-independent pixels. This value does not depend on the \ + * size of the configured font. If you want to take font sizes into account when scaling elements, + * use theme.mSize(theme.defaultFont), units.smallSpacing and units.largeSpacing. + * The devicePixelRatio follows the definition of "device independent pixel" by Microsoft. + * + * @deprecated since 5.86. When using Qt's high DPI scaling, all sizes are + * considered to be device-independent pixels, so this will simply return 1. + */ + Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY devicePixelRatioChanged) + + /** + * units.veryLongDuration should be used for specialty animations that benefit + * from being even longer than longDuration. + */ + Q_PROPERTY(int veryLongDuration READ veryLongDuration WRITE setVeryLongDuration NOTIFY veryLongDurationChanged) + + /** + * 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 WRITE setLongDuration NOTIFY longDurationChanged) + + /** + * units.shortDuration should be used for short animations, such as accentuating a UI event, + * hover events, etc.. + */ + Q_PROPERTY(int shortDuration READ shortDuration WRITE setShortDuration NOTIFY shortDurationChanged) + + /** + * 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 WRITE setVeryShortDuration NOTIFY veryShortDurationChanged) + + /** + * 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 WRITE setHumanMoment NOTIFY humanMomentChanged) + + /** + * time in ms by which the display of tooltips will be delayed. + * + * @sa ToolTip.delay property + */ + Q_PROPERTY(int toolTipDelay READ toolTipDelay WRITE setToolTipDelay NOTIFY toolTipDelayChanged) + +#if KIRIGAMI2_ENABLE_DEPRECATED_SINCE(5, 86) + /** + * How much the mouse scroll wheel scrolls, expressed in lines of text. + * Note: this is strictly for classical mouse wheels, touchpads 2 figer scrolling won't be affected + */ + Q_PROPERTY(int wheelScrollLines READ wheelScrollLines NOTIFY wheelScrollLinesChanged) +#endif + +#if KIRIGAMI2_ENABLE_DEPRECATED_SINCE(5, 86) + /** + * metrics used by the default font + * + * @deprecated since 5.86.0, Create your own TextMetrics object if needed. + * For the roundedIconSize function, use Units.iconSizes.roundedIconSize instead + */ + Q_PROPERTY(QObject *fontMetrics READ fontMetrics CONSTANT) +#endif + +public: + explicit Units(QObject *parent = nullptr); + ~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); + +#if KIRIGAMI2_ENABLE_DEPRECATED_SINCE(5, 86) + // TODO KF6 remove + KIRIGAMI2_DEPRECATED_VERSION(5, 86, "When using Qt scaling, this would return a value of 1") + qreal devicePixelRatio() const; +#endif + + 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); + +#if KIRIGAMI2_ENABLE_DEPRECATED_SINCE(5, 86) + // TODO KF6 remove + KIRIGAMI2_DEPRECATED_VERSION(5, 86, "Use Qt.styleHints.wheelScrollLines instead") + int wheelScrollLines() const; + void setWheelScrollLines(int lines); +#endif + + IconSizes *iconSizes() const; + +Q_SIGNALS: + void gridUnitChanged(); + void smallSpacingChanged(); + void mediumSpacingChanged(); + void largeSpacingChanged(); + void devicePixelRatioChanged(); + void veryLongDurationChanged(); + void longDurationChanged(); + void shortDurationChanged(); + void veryShortDurationChanged(); + void humanMomentChanged(); + void toolTipDelayChanged(); + void wheelScrollLinesChanged(); + +private: +#if KIRIGAMI2_ENABLE_DEPRECATED_SINCE(5, 86) + QObject *fontMetrics() const; +#endif + + std::unique_ptr d; +}; + +} diff --git a/src/libkirigami/virtualkeyboardwatcher.cpp b/src/libkirigami/virtualkeyboardwatcher.cpp new file mode 100644 index 0000000..852b00c --- /dev/null +++ b/src/libkirigami/virtualkeyboardwatcher.cpp @@ -0,0 +1,223 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "virtualkeyboardwatcher.h" + +#ifdef KIRIGAMI_ENABLE_DBUS +#include "virtualkeyboard_interface.h" +#include +#include +#endif + +#include "loggingcategory.h" + +namespace Kirigami +{ +Q_GLOBAL_STATIC(VirtualKeyboardWatcher, virtualKeyboardWatcherSelf) + +class Q_DECL_HIDDEN VirtualKeyboardWatcher::Private +{ +public: + Private(VirtualKeyboardWatcher *qq) + : q(qq) + { + } + + VirtualKeyboardWatcher *q; + +#ifdef KIRIGAMI_ENABLE_DBUS + void getAllProperties(); + void getProperty(const QString &propertyName); + void updateWillShowOnActive(); + + OrgKdeKwinVirtualKeyboardInterface *keyboardInterface = nullptr; + OrgFreedesktopDBusPropertiesInterface *propertiesInterface = nullptr; + + QDBusPendingCallWatcher *willShowOnActiveCall = nullptr; +#endif + + bool available = false; + bool enabled = false; + bool active = false; + bool visible = false; + bool willShowOnActive = false; + + static const QString serviceName; + static const QString objectName; + static const QString interfaceName; +}; + +const QString VirtualKeyboardWatcher::Private::serviceName = QStringLiteral("org.kde.KWin"); +const QString VirtualKeyboardWatcher::Private::objectName = QStringLiteral("/VirtualKeyboard"); +const QString VirtualKeyboardWatcher::Private::interfaceName = QStringLiteral("org.kde.kwin.VirtualKeyboard"); + +VirtualKeyboardWatcher::VirtualKeyboardWatcher(QObject *parent) + : QObject(parent) + , d(std::make_unique(this)) +{ +#ifdef KIRIGAMI_ENABLE_DBUS + d->keyboardInterface = new OrgKdeKwinVirtualKeyboardInterface(Private::serviceName, Private::objectName, QDBusConnection::sessionBus(), this); + d->propertiesInterface = new OrgFreedesktopDBusPropertiesInterface(Private::serviceName, Private::objectName, QDBusConnection::sessionBus(), this); + + connect(d->keyboardInterface, &OrgKdeKwinVirtualKeyboardInterface::availableChanged, this, [this]() { + d->getProperty(QStringLiteral("available")); + }); + connect(d->keyboardInterface, &OrgKdeKwinVirtualKeyboardInterface::enabledChanged, this, [this]() { + d->getProperty(QStringLiteral("enabled")); + }); + connect(d->keyboardInterface, &OrgKdeKwinVirtualKeyboardInterface::activeChanged, this, [this]() { + d->getProperty(QStringLiteral("active")); + }); + connect(d->keyboardInterface, &OrgKdeKwinVirtualKeyboardInterface::visibleChanged, this, [this]() { + d->getProperty(QStringLiteral("visible")); + }); + + d->getAllProperties(); +#endif +} + +VirtualKeyboardWatcher::~VirtualKeyboardWatcher() = default; + +bool VirtualKeyboardWatcher::available() const +{ + return d->available; +} + +bool VirtualKeyboardWatcher::enabled() const +{ + return d->enabled; +} + +void VirtualKeyboardWatcher::setEnabled(bool newEnabled) +{ + if (newEnabled == d->enabled) { + return; + } + + d->enabled = newEnabled; + +#ifdef KIRIGAMI_ENABLE_DBUS + d->propertiesInterface->Set(Private::interfaceName, QStringLiteral("enabled"), QDBusVariant(newEnabled)); +#else + Q_EMIT enabledChanged(); +#endif +} + +bool VirtualKeyboardWatcher::active() const +{ + return d->active; +} + +void VirtualKeyboardWatcher::setActive(bool newActive) +{ + if (newActive == d->active) { + return; + } + + d->active = newActive; + +#ifdef KIRIGAMI_ENABLE_DBUS + d->propertiesInterface->Set(Private::interfaceName, QStringLiteral("active"), QDBusVariant(newActive)); +#else + Q_EMIT activeChanged(); +#endif +} + +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(keyboardInterface->willShowOnActive(), q); + connect(willShowOnActiveCall, &QDBusPendingCallWatcher::finished, q, [this](auto call) { + QDBusPendingReply reply = *call; + if (reply.isError()) { + qCDebug(KirigamiLog) << reply.error().message(); + } else { + if (reply.value() != willShowOnActive) { + willShowOnActive = reply.value(); + Q_EMIT q->willShowOnActiveChanged(); + } + } + call->deleteLater(); + willShowOnActiveCall = nullptr; + }); +} + +void VirtualKeyboardWatcher::Private::getAllProperties() +{ + auto call = new QDBusPendingCallWatcher(propertiesInterface->GetAll(interfaceName), q); + connect(call, &QDBusPendingCallWatcher::finished, q, [this](auto call) { + QDBusPendingReply reply = *call; + if (reply.isError()) { + qCDebug(KirigamiLog) << reply.error().message(); + } else { + auto value = reply.value(); + available = value.value(QStringLiteral("available")).toBool(); + enabled = value.value(QStringLiteral("enabled")).toBool(); + active = value.value(QStringLiteral("active")).toBool(); + visible = value.value(QStringLiteral("visible")).toBool(); + } + call->deleteLater(); + + Q_EMIT q->availableChanged(); + Q_EMIT q->enabledChanged(); + Q_EMIT q->activeChanged(); + Q_EMIT q->visibleChanged(); + }); +} + +void VirtualKeyboardWatcher::Private::getProperty(const QString &propertyName) +{ + auto call = new QDBusPendingCallWatcher(propertiesInterface->Get(interfaceName, propertyName), q); + connect(call, &QDBusPendingCallWatcher::finished, q, [this, propertyName](auto call) { + QDBusPendingReply reply = *call; + if (reply.isError()) { + qCDebug(KirigamiLog) << reply.error().message(); + } else { + auto value = reply.value(); + if (propertyName == QStringLiteral("available")) { + available = value.variant().toBool(); + Q_EMIT q->availableChanged(); + } else if (propertyName == QStringLiteral("enabled")) { + enabled = value.variant().toBool(); + Q_EMIT q->enabledChanged(); + } else if (propertyName == QStringLiteral("active")) { + active = value.variant().toBool(); + Q_EMIT q->activeChanged(); + } else if (propertyName == QStringLiteral("visible")) { + visible = value.variant().toBool(); + Q_EMIT q->visibleChanged(); + } + } + call->deleteLater(); + }); +} + +#endif +} diff --git a/src/libkirigami/virtualkeyboardwatcher.h b/src/libkirigami/virtualkeyboardwatcher.h new file mode 100644 index 0000000..8579544 --- /dev/null +++ b/src/libkirigami/virtualkeyboardwatcher.h @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2018 Marco Martin + * SPDX-FileCopyrightText: 2021 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef KIRIGAMI_VIRTUALKEYBOARDWATCHER +#define KIRIGAMI_VIRTUALKEYBOARDWATCHER + +#include + +#include + +#include "kirigami2_export.h" + +namespace Kirigami +{ +/** + * @class VirtualKeyboardWatcher virtualkeyboardwatcher.h + * + * This class reports on the status of KWin's VirtualKeyboard DBus interface. + * + * @since 5.91 + */ +class KIRIGAMI2_EXPORT VirtualKeyboardWatcher : public QObject +{ + Q_OBJECT + +public: + VirtualKeyboardWatcher(QObject *parent = nullptr); + ~VirtualKeyboardWatcher(); + + Q_PROPERTY(bool available READ available NOTIFY availableChanged) + bool available() const; + Q_SIGNAL void availableChanged(); + + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + bool enabled() const; + void setEnabled(bool newEnabled); + Q_SIGNAL void enabledChanged(); + + Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged) + bool active() const; + void setActive(bool newActive); + Q_SIGNAL void activeChanged(); + + Q_PROPERTY(bool visible READ visible NOTIFY visibleChanged) + bool visible() const; + Q_SIGNAL void visibleChanged(); + + Q_PROPERTY(bool willShowOnActive READ willShowOnActive NOTIFY willShowOnActiveChanged) + 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/mnemonicattached.cpp b/src/mnemonicattached.cpp new file mode 100644 index 0000000..605b77f --- /dev/null +++ b/src/mnemonicattached.cpp @@ -0,0 +1,458 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "mnemonicattached.h" +#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; +} + +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; +} + +MnemonicAttached::MnemonicAttached(QObject *parent) + : QObject(parent) +{ + QQuickItem *parentItem = qobject_cast(parent); + if (parentItem) { + if (parentItem->window()) { + m_window = parentItem->window(); + m_window->installEventFilter(this); + } + connect(parentItem, &QQuickItem::windowChanged, this, [this](QQuickWindow *window) { + removeEventFilterForWindow(m_window); + m_window = window; + installEventFilterForWindow(m_window); + }); + } +} + +MnemonicAttached::~MnemonicAttached() +{ + s_sequenceToObject.remove(m_sequence); +} + +bool MnemonicAttached::eventFilter(QObject *watched, QEvent *e) +{ + Q_UNUSED(watched) + + if (m_richTextLabel.isEmpty()) { + return false; + } + + if (e->type() == QEvent::KeyPress) { + QKeyEvent *ke = static_cast(e); + if (ke->key() == Qt::Key_Alt) { + m_actualRichTextLabel = m_richTextLabel; + Q_EMIT richTextLabelChanged(); + m_active = true; + Q_EMIT activeChanged(); + } + + } else if (e->type() == QEvent::KeyRelease) { + QKeyEvent *ke = static_cast(e); + if (ke->key() == Qt::Key_Alt) { + m_actualRichTextLabel = m_label; + m_actualRichTextLabel = removeAcceleratorMarker(m_actualRichTextLabel); + Q_EMIT richTextLabelChanged(); + m_active = false; + Q_EMIT activeChanged(); + } + } + return false; +} + +// 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 + (m_weights.cend() - 1).key(); + } +} + +bool MnemonicAttached::installEventFilterForWindow(QQuickWindow *wnd) +{ + if (!wnd) { + return false; + } + QWindow *renderWindow = QQuickRenderControl::renderWindowFor(wnd); + // renderWindow means the widget is rendering somewhere else, like a QQuickWidget + if (renderWindow && renderWindow != m_window) { + renderWindow->installEventFilter(this); + } else { + wnd->installEventFilter(this); + } + return true; +} + +bool MnemonicAttached::removeEventFilterForWindow(QQuickWindow *wnd) +{ + if (!wnd) { + return false; + } + QWindow *renderWindow = QQuickRenderControl::renderWindowFor(wnd); + if (renderWindow) { + renderWindow->removeEventFilter(this); + } else { + wnd->removeEventFilter(this); + } + return true; +} + +void MnemonicAttached::updateSequence() +{ + 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("&& ")); + + if (!m_enabled) { + m_actualRichTextLabel = text; + m_actualRichTextLabel = removeAcceleratorMarker(m_actualRichTextLabel); + // 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(); + } + return; + } + + 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_actualRichTextLabel = m_richTextLabel; + 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.isEmpty()) { + Q_EMIT sequenceChanged(); + } + m_actualRichTextLabel = text; + m_actualRichTextLabel = removeAcceleratorMarker(m_actualRichTextLabel); + m_mnemonicLabel = m_actualRichTextLabel; + + Q_EMIT richTextLabelChanged(); + Q_EMIT mnemonicLabelChanged(); +} + +void MnemonicAttached::setLabel(const QString &text) +{ + if (m_label == text) { + return; + } + + m_label = text; + updateSequence(); + Q_EMIT labelChanged(); +} + +QString MnemonicAttached::richTextLabel() const +{ + return !m_actualRichTextLabel.isEmpty() ? m_actualRichTextLabel : m_label; +} + +QString MnemonicAttached::mnemonicLabel() const +{ + return m_mnemonicLabel; +} + +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 + (m_weights.constEnd() - 1).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) { + removeEventFilterForWindow(m_window); + if (m_actualRichTextLabel != m_richTextLabel) { + m_actualRichTextLabel = m_richTextLabel; + Q_EMIT richTextLabelChanged(); + } + + } else { + installEventFilterForWindow(m_window); + m_actualRichTextLabel = m_label; + m_actualRichTextLabel = removeAcceleratorMarker(m_actualRichTextLabel); + Q_EMIT richTextLabelChanged(); + } + + Q_EMIT activeChanged(); +} + +#include "moc_mnemonicattached.cpp" diff --git a/src/mnemonicattached.h b/src/mnemonicattached.h new file mode 100644 index 0000000..477aaaf --- /dev/null +++ b/src/mnemonicattached.h @@ -0,0 +1,169 @@ +/* + * 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 + /** + * 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) + + /** + * 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) + + /** + * The label with an "&" mnemonic in the place which will have the shortcut + * assigned, regardless the & wasassigned by the user or automatically generated. + */ + Q_PROPERTY(QString mnemonicLabel READ mnemonicLabel NOTIFY mnemonicLabelChanged) + + /** + * Only if true this mnemonic will be considered for the global assignment + * default: true + */ + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + + /** + * 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) + + /** + * The final key sequence assigned, if any: it will be Alt+alphanumeric char + */ + Q_PROPERTY(QKeySequence sequence READ sequence NOTIFY sequenceChanged) + + /** + * 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) + +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; + + 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: + bool eventFilter(QObject *watched, QEvent *e) override; + void updateSequence(); + +Q_SIGNALS: + void labelChanged(); + void enabledChanged(); + void sequenceChanged(); + void richTextLabelChanged(); + void mnemonicLabelChanged(); + void controlTypeChanged(); + void activeChanged(); + +private: + void calculateWeights(); + bool installEventFilterForWindow(QQuickWindow *wnd); + bool removeEventFilterForWindow(QQuickWindow *wnd); + + // 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/pagepool.cpp b/src/pagepool.cpp new file mode 100644 index 0000000..89c222a --- /dev/null +++ b/src/pagepool.cpp @@ -0,0 +1,302 @@ +/* + * 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 +{ + auto items = m_itemForUrl.values(); + return QList(items.cbegin(), items.cend()); +} + +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) +{ + Q_ASSERT(qmlEngine(this)); + QQmlContext *ctx = QQmlEngine::contextForObject(this); + Q_ASSERT(ctx); + + 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 = {qmlEngine(this)->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(qmlEngine(this), 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, 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 = {qmlEngine(this)->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 = {qmlEngine(this)->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) +{ + QQmlContext *ctx = QQmlEngine::contextForObject(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 +{ + Q_ASSERT(qmlEngine(this)); + QQmlContext *ctx = QQmlEngine::contextForObject(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 (auto *c : std::as_const(m_componentForUrl)) { + c->deleteLater(); + } + m_componentForUrl.clear(); + + for (auto *i : std::as_const(m_itemForUrl)) { + // items that had been deparented are safe to delete + if (!i->parentItem()) { + i->deleteLater(); + } + QQmlEngine::setObjectOwnership(i, 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..fd71e87 --- /dev/null +++ b/src/pagepool.h @@ -0,0 +1,146 @@ +/* + * 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 + /** + * 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) + + /** + * The last item that was loaded with @loadPage. + */ + Q_PROPERTY(QQuickItem *lastLoadedItem READ lastLoadedItem NOTIFY lastLoadedItemChanged) + + /** + * All items loaded/managed by the PagePool. + * @since 5.84 + */ + Q_PROPERTY(QList items READ items NOTIFY itemsChanged) + + /** + * All page URLs loaded/managed by the PagePool. + * @since 5.84 + */ + Q_PROPERTY(QList urls READ urls NOTIFY urlsChanged) + + /** + * 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) + +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/pagerouter.cpp b/src/pagerouter.cpp new file mode 100644 index 0000000..d4662c5 --- /dev/null +++ b/src/pagerouter.cpp @@ -0,0 +1,782 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "pagerouter.h" +#include "loggingcategory.h" +#include +#include +#include +#include +#include +#include +#include +#include + +ParsedRoute *parseRoute(QJSValue value) +{ + if (value.isUndefined()) { + return new ParsedRoute{}; + } else if (value.isString()) { + return new ParsedRoute{value.toString(), QVariant()}; + } else { + auto map = value.toVariant().value(); + map.remove(QStringLiteral("route")); + map.remove(QStringLiteral("data")); + return new ParsedRoute{value.property(QStringLiteral("route")).toString(), // + value.property(QStringLiteral("data")).toVariant(), + map, + false}; + } +} + +QList parseRoutes(QJSValue values) +{ + QList ret; + if (values.isArray()) { + const auto valuesList = values.toVariant().toList(); + for (const auto &route : valuesList) { + if (route.toString() != QString()) { + ret << new ParsedRoute{route.toString(), QVariant(), QVariantMap(), false, nullptr}; + } else if (route.canConvert()) { + auto map = route.value(); + auto copy = map; + copy.remove(QStringLiteral("route")); + copy.remove(QStringLiteral("data")); + + ret << new ParsedRoute{map.value(QStringLiteral("route")).toString(), map.value(QStringLiteral("data")), copy, false, nullptr}; + } + } + } else { + ret << parseRoute(values); + } + return ret; +} + +PageRouter::PageRouter(QQuickItem *parent) + : QObject(parent) + , m_paramMap(new QQmlPropertyMap) + , m_cache() + , m_preload() +{ + connect(this, &PageRouter::pageStackChanged, [=]() { + connect(m_pageStack, &ColumnView::currentIndexChanged, this, &PageRouter::currentIndexChanged); + }); +} + +QQmlListProperty PageRouter::routes() +{ + return QQmlListProperty(this, nullptr, appendRoute, routeCount, route, clearRoutes); +} + +void PageRouter::appendRoute(QQmlListProperty *prop, PageRoute *route) +{ + auto router = qobject_cast(prop->object); + router->m_routes.append(route); +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +int PageRouter::routeCount(QQmlListProperty *prop) +#else +qsizetype PageRouter::routeCount(QQmlListProperty *prop) +#endif +{ + auto router = qobject_cast(prop->object); + return router->m_routes.length(); +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +PageRoute *PageRouter::route(QQmlListProperty *prop, int index) +#else +PageRoute *PageRouter::route(QQmlListProperty *prop, qsizetype index) +#endif +{ + auto router = qobject_cast(prop->object); + return router->m_routes[index]; +} + +void PageRouter::clearRoutes(QQmlListProperty *prop) +{ + auto router = qobject_cast(prop->object); + router->m_routes.clear(); +} + +PageRouter::~PageRouter() +{ +} + +void PageRouter::classBegin() +{ +} + +void PageRouter::componentComplete() +{ + if (m_pageStack == nullptr) { + qCCritical(KirigamiLog) + << "PageRouter should be created with a ColumnView. Not doing so is undefined behaviour, and is likely to result in a crash upon further " + "interaction."; + } else { + Q_EMIT pageStackChanged(); + m_currentRoutes.clear(); + push(parseRoute(initialRoute())); + } +} + +bool PageRouter::routesContainsKey(const QString &key) const +{ + for (auto route : m_routes) { + if (route->name() == key) { + return true; + } + } + return false; +} + +QQmlComponent *PageRouter::routesValueForKey(const QString &key) const +{ + for (auto route : m_routes) { + if (route->name() == key) { + return route->component(); + } + } + return nullptr; +} + +bool PageRouter::routesCacheForKey(const QString &key) const +{ + for (auto route : m_routes) { + if (route->name() == key) { + return route->cache(); + } + } + return false; +} + +int PageRouter::routesCostForKey(const QString &key) const +{ + for (auto route : m_routes) { + if (route->name() == key) { + return route->cost(); + } + } + return -1; +} + +// It would be nice if this could surgically update the +// param map instead of doing this brute force approach, +// but this seems to work well enough, and prematurely +// optimising stuff is pretty bad if it isn't found as +// a performance bottleneck. +void PageRouter::reevaluateParamMapProperties() +{ + QStringList currentKeys; + + for (auto item : m_currentRoutes) { + for (auto key : item->properties.keys()) { + currentKeys << key; + + auto &value = item->properties[key]; + m_paramMap->insert(key, value); + } + } + + for (auto key : m_paramMap->keys()) { + if (!currentKeys.contains(key)) { + m_paramMap->clear(key); + } + } +} + +void PageRouter::push(ParsedRoute *route) +{ + Q_ASSERT(route); + if (!routesContainsKey(route->name)) { + qCCritical(KirigamiLog) << "Route" << route->name << "not defined"; + return; + } + if (routesCacheForKey(route->name)) { + auto push = [route, this](ParsedRoute *item) { + m_currentRoutes << item; + + for (auto it = route->properties.begin(); it != route->properties.end(); it++) { + item->item->setProperty(qUtf8Printable(it.key()), it.value()); + item->properties[it.key()] = it.value(); + } + reevaluateParamMapProperties(); + + m_pageStack->addItem(item->item); + }; + auto item = m_cache.take(qMakePair(route->name, route->hash())); + if (item && item->item) { + push(item); + return; + } + item = m_preload.take(qMakePair(route->name, route->hash())); + if (item && item->item) { + push(item); + return; + } + } + auto context = qmlContext(this); + auto component = routesValueForKey(route->name); + auto createAndPush = [component, context, route, this]() { + // We use beginCreate and completeCreate to allow + // for a PageRouterAttached to find its parent + // on construction time. + auto item = component->beginCreate(context); + if (item == nullptr) { + return; + } + item->setParent(this); + auto qqItem = qobject_cast(item); + if (!qqItem) { + qCCritical(KirigamiLog) << "Route" << route->name << "is not an item! This is undefined behaviour and will likely crash your application."; + } + for (auto it = route->properties.begin(); it != route->properties.end(); it++) { + qqItem->setProperty(qUtf8Printable(it.key()), it.value()); + } + route->setItem(qqItem); + route->cache = routesCacheForKey(route->name); + m_currentRoutes << route; + reevaluateParamMapProperties(); + + auto attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + attached->m_router = this; + component->completeCreate(); + m_pageStack->addItem(qqItem); + m_pageStack->setCurrentIndex(m_currentRoutes.length() - 1); + }; + + if (component->status() == QQmlComponent::Ready) { + createAndPush(); + } else if (component->status() == QQmlComponent::Loading) { + connect(component, &QQmlComponent::statusChanged, [=](QQmlComponent::Status status) { + // Loading can only go to Ready or Error. + if (status != QQmlComponent::Ready) { + qCCritical(KirigamiLog) << "Failed to push route:" << component->errors(); + } + createAndPush(); + }); + } else { + qCCritical(KirigamiLog) << "Failed to push route:" << component->errors(); + } +} + +QJSValue PageRouter::initialRoute() const +{ + return m_initialRoute; +} + +void PageRouter::setInitialRoute(QJSValue value) +{ + m_initialRoute = value; +} + +void PageRouter::navigateToRoute(QJSValue route) +{ + auto incomingRoutes = parseRoutes(route); + QList resolvedRoutes; + + if (incomingRoutes.length() <= m_currentRoutes.length()) { + resolvedRoutes = m_currentRoutes.mid(0, incomingRoutes.length()); + } else { + resolvedRoutes = m_currentRoutes; + resolvedRoutes.reserve(incomingRoutes.length() - m_currentRoutes.length()); + } + + for (int i = 0; i < incomingRoutes.length(); i++) { + auto incoming = incomingRoutes.at(i); + Q_ASSERT(incoming); + if (i >= resolvedRoutes.length()) { + resolvedRoutes.append(incoming); + } else { + auto current = resolvedRoutes.value(i); + Q_ASSERT(current); + auto props = incoming->properties; + if (current->name != incoming->name || current->data != incoming->data) { + resolvedRoutes.replace(i, incoming); + } + resolvedRoutes[i]->properties.clear(); + for (auto it = props.constBegin(); it != props.constEnd(); it++) { + resolvedRoutes[i]->properties.insert(it.key(), it.value()); + } + } + } + + for (const auto &route : std::as_const(m_currentRoutes)) { + if (!resolvedRoutes.contains(route)) { + placeInCache(route); + } + } + + m_pageStack->clear(); + m_currentRoutes.clear(); + for (auto toPush : std::as_const(resolvedRoutes)) { + push(toPush); + } + reevaluateParamMapProperties(); + Q_EMIT navigationChanged(); +} + +void PageRouter::bringToView(QJSValue route) +{ + if (route.isNumber()) { + auto index = route.toNumber(); + m_pageStack->setCurrentIndex(index); + } else { + auto parsed = parseRoute(route); + auto index = 0; + for (auto currentRoute : std::as_const(m_currentRoutes)) { + if (currentRoute->name == parsed->name && currentRoute->data == parsed->data) { + m_pageStack->setCurrentIndex(index); + return; + } + index++; + } + qCWarning(KirigamiLog) << "Route" << parsed->name << "with data" << parsed->data << "is not on the current stack of routes."; + } +} + +bool PageRouter::routeActive(QJSValue route) +{ + auto parsed = parseRoutes(route); + if (parsed.length() > m_currentRoutes.length()) { + return false; + } + for (int i = 0; i < parsed.length(); i++) { + if (parsed[i]->name != m_currentRoutes[i]->name) { + return false; + } + if (parsed[i]->data.isValid()) { + if (parsed[i]->data != m_currentRoutes[i]->data) { + return false; + } + } + } + return true; +} + +void PageRouter::pushRoute(QJSValue route) +{ + push(parseRoute(route)); + Q_EMIT navigationChanged(); +} + +void PageRouter::popRoute() +{ + m_pageStack->pop(m_currentRoutes.last()->item); + placeInCache(m_currentRoutes.last()); + m_currentRoutes.removeLast(); + reevaluateParamMapProperties(); + Q_EMIT navigationChanged(); +} + +QVariant PageRouter::dataFor(QObject *object) +{ + auto pointer = object; + auto qqiPointer = qobject_cast(object); + QHash routes; + for (auto route : std::as_const(m_cache.items)) { + routes[route->item] = route; + } + for (auto route : std::as_const(m_preload.items)) { + routes[route->item] = route; + } + for (auto route : std::as_const(m_currentRoutes)) { + routes[route->item] = route; + } + while (qqiPointer != nullptr) { + const auto keys = routes.keys(); + for (auto item : keys) { + if (item == qqiPointer) { + return routes[item]->data; + } + } + qqiPointer = qqiPointer->parentItem(); + } + while (pointer != nullptr) { + const auto keys = routes.keys(); + for (auto item : keys) { + if (item == pointer) { + return routes[item]->data; + } + } + pointer = pointer->parent(); + } + return QVariant(); +} + +bool PageRouter::isActive(QObject *object) +{ + auto pointer = object; + while (pointer != nullptr) { + auto index = 0; + for (auto route : std::as_const(m_currentRoutes)) { + if (route->item == pointer) { + return m_pageStack->currentIndex() == index; + } + index++; + } + pointer = pointer->parent(); + } + qCWarning(KirigamiLog) << "Object" << object << "not in current routes"; + return false; +} + +PageRouterAttached *PageRouter::qmlAttachedProperties(QObject *object) +{ + auto attached = new PageRouterAttached(object); + return attached; +} + +QSet flatParentTree(QObject *object) +{ + // See below comment in Climber::climbObjectParents for why this is here. + static const QMetaObject *metaObject = QMetaType::metaObjectForType(QMetaType::type("QQuickItem*")); + QSet ret; + // Use an inline struct type so that climbItemParents and climbObjectParents + // can call each other + struct Climber { + void climbItemParents(QSet &out, QQuickItem *item) + { + auto parent = item->parentItem(); + while (parent != nullptr) { + out << parent; + climbObjectParents(out, parent); + parent = parent->parentItem(); + } + } + void climbObjectParents(QSet &out, QObject *object) + { + auto parent = object->parent(); + while (parent != nullptr) { + out << parent; + // We manually call metaObject()->inherits() and + // use a reinterpret cast because qobject_cast seems + // to have stability issues here due to mutable + // pointer mechanics. + if (parent->metaObject()->inherits(metaObject)) { + climbItemParents(out, reinterpret_cast(parent)); + } + parent = parent->parent(); + } + } + }; + Climber climber; + if (qobject_cast(object)) { + climber.climbItemParents(ret, qobject_cast(object)); + } + climber.climbObjectParents(ret, object); + return ret; +} + +void PageRouter::preload(ParsedRoute *route) +{ + for (auto preloaded : std::as_const(m_preload.items)) { + if (preloaded->equals(route)) { + delete route; + return; + } + } + if (!routesContainsKey(route->name)) { + qCCritical(KirigamiLog) << "Route" << route->name << "not defined"; + delete route; + return; + } + auto context = qmlContext(this); + auto component = routesValueForKey(route->name); + auto createAndCache = [component, context, route, this]() { + auto item = component->beginCreate(context); + item->setParent(this); + auto qqItem = qobject_cast(item); + if (!qqItem) { + qCCritical(KirigamiLog) << "Route" << route->name << "is not an item! This is undefined behaviour and will likely crash your application."; + } + for (auto it = route->properties.begin(); it != route->properties.end(); it++) { + qqItem->setProperty(qUtf8Printable(it.key()), it.value()); + } + route->setItem(qqItem); + route->cache = routesCacheForKey(route->name); + auto attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + attached->m_router = this; + component->completeCreate(); + if (!route->cache) { + qCCritical(KirigamiLog) << "Route" << route->name << "is being preloaded despite it not having caching enabled."; + delete route; + return; + } + auto string = route->name; + auto hash = route->hash(); + m_preload.insert(qMakePair(string, hash), route, routesCostForKey(route->name)); + }; + + if (component->status() == QQmlComponent::Ready) { + createAndCache(); + } else if (component->status() == QQmlComponent::Loading) { + connect(component, &QQmlComponent::statusChanged, [=](QQmlComponent::Status status) { + // Loading can only go to Ready or Error. + if (status != QQmlComponent::Ready) { + qCCritical(KirigamiLog) << "Failed to push route:" << component->errors(); + } + createAndCache(); + }); + } else { + qCCritical(KirigamiLog) << "Failed to push route:" << component->errors(); + } +} + +void PageRouter::unpreload(ParsedRoute *route) +{ + ParsedRoute *toDelete = nullptr; + for (auto preloaded : std::as_const(m_preload.items)) { + if (preloaded->equals(route)) { + toDelete = preloaded; + } + } + if (toDelete != nullptr) { + m_preload.take(qMakePair(toDelete->name, toDelete->hash())); + delete toDelete; + } + delete route; +} + +void PreloadRouteGroup::handleChange() +{ + if (!(m_parent->m_router)) { + qCCritical(KirigamiLog) << "PreloadRouteGroup does not have a parent PageRouter"; + return; + } + auto r = m_parent->m_router; + auto parsed = parseRoute(m_route); + if (m_when) { + r->preload(parsed); + } else { + r->unpreload(parsed); + } +} + +PreloadRouteGroup::~PreloadRouteGroup() +{ + if (m_parent->m_router) { + m_parent->m_router->unpreload(parseRoute(m_route)); + } +} + +void PageRouterAttached::findParent() +{ + QQuickItem *parent = qobject_cast(this->parent()); + while (parent != nullptr) { + auto attached = qobject_cast(qmlAttachedPropertiesObject(parent, false)); + if (attached != nullptr && attached->m_router != nullptr) { + m_router = attached->m_router; + Q_EMIT routerChanged(); + Q_EMIT dataChanged(); + Q_EMIT isCurrentChanged(); + Q_EMIT navigationChanged(); + break; + } + parent = parent->parentItem(); + } +} + +void PageRouterAttached::navigateToRoute(QJSValue route) +{ + if (m_router) { + m_router->navigateToRoute(route); + } else { + qCCritical(KirigamiLog) << "PageRouterAttached does not have a parent PageRouter"; + return; + } +} + +bool PageRouterAttached::routeActive(QJSValue route) +{ + if (m_router) { + return m_router->routeActive(route); + } else { + qCCritical(KirigamiLog) << "PageRouterAttached does not have a parent PageRouter"; + return false; + } +} + +void PageRouterAttached::pushRoute(QJSValue route) +{ + if (m_router) { + m_router->pushRoute(route); + } else { + qCCritical(KirigamiLog) << "PageRouterAttached does not have a parent PageRouter"; + return; + } +} + +void PageRouterAttached::popRoute() +{ + if (m_router) { + m_router->popRoute(); + } else { + qCCritical(KirigamiLog) << "PageRouterAttached does not have a parent PageRouter"; + return; + } +} + +void PageRouterAttached::bringToView(QJSValue route) +{ + if (m_router) { + m_router->bringToView(route); + } else { + qCCritical(KirigamiLog) << "PageRouterAttached does not have a parent PageRouter"; + return; + } +} + +QVariant PageRouterAttached::data() const +{ + if (m_router) { + return m_router->dataFor(parent()); + } else { + qCCritical(KirigamiLog) << "PageRouterAttached does not have a parent PageRouter"; + return QVariant(); + } +} + +bool PageRouterAttached::isCurrent() const +{ + if (m_router) { + return m_router->isActive(parent()); + } else { + qCCritical(KirigamiLog) << "PageRouterAttached does not have a parent PageRouter"; + return false; + } +} + +bool PageRouterAttached::watchedRouteActive() +{ + if (m_router) { + return m_router->routeActive(m_watchedRoute); + } else { + qCCritical(KirigamiLog) << "PageRouterAttached does not have a parent PageRouter"; + return false; + } +} + +void PageRouterAttached::setWatchedRoute(QJSValue route) +{ + m_watchedRoute = route; + Q_EMIT watchedRouteChanged(); +} + +QJSValue PageRouterAttached::watchedRoute() +{ + return m_watchedRoute; +} + +void PageRouterAttached::pushFromHere(QJSValue route) +{ + if (m_router) { + m_router->pushFromObject(parent(), route); + } else { + qCCritical(KirigamiLog) << "PageRouterAttached does not have a parent PageRouter"; + } +} + +void PageRouterAttached::replaceFromHere(QJSValue route) +{ + if (m_router) { + m_router->pushFromObject(parent(), route, true); + } else { + qCCritical(KirigamiLog) << "PageRouterAttached does not have a parent PageRouter"; + } +} + +void PageRouterAttached::popFromHere() +{ + if (m_router) { + m_router->pushFromObject(parent(), QJSValue()); + } else { + qCCritical(KirigamiLog) << "PageRouterAttached does not have a parent PageRouter"; + } +} + +void PageRouter::placeInCache(ParsedRoute *route) +{ + Q_ASSERT(route); + if (!route->cache) { + delete route; + return; + } + auto string = route->name; + auto hash = route->hash(); + m_cache.insert(qMakePair(string, hash), route, routesCostForKey(route->name)); +} + +void PageRouter::pushFromObject(QObject *object, QJSValue inputRoute, bool replace) +{ + const auto parsed = parseRoutes(inputRoute); + const auto objects = flatParentTree(object); + + for (const auto &obj : objects) { + bool popping = false; + for (auto route : std::as_const(m_currentRoutes)) { + if (popping) { + m_currentRoutes.removeAll(route); + reevaluateParamMapProperties(); + placeInCache(route); + continue; + } + if (route->item == obj) { + m_pageStack->pop(route->item); + if (replace) { + m_currentRoutes.removeAll(route); + reevaluateParamMapProperties(); + m_pageStack->removeItem(route->item); + } + popping = true; + } + } + if (popping) { + if (!inputRoute.isUndefined()) { + for (auto route : parsed) { + push(route); + } + } + Q_EMIT navigationChanged(); + return; + } + } + qCWarning(KirigamiLog) << "Object" << object << "not in current routes"; +} + +QJSValue PageRouter::currentRoutes() const +{ + auto engine = qjsEngine(this); + auto ret = engine->newArray(m_currentRoutes.length()); + for (int i = 0; i < m_currentRoutes.length(); ++i) { + auto object = engine->newObject(); + object.setProperty(QStringLiteral("route"), m_currentRoutes[i]->name); + object.setProperty(QStringLiteral("data"), engine->toScriptValue(m_currentRoutes[i]->data)); + auto keys = m_currentRoutes[i]->properties.keys(); + for (auto key : keys) { + object.setProperty(key, engine->toScriptValue(m_currentRoutes[i]->properties[key])); + } + ret.setProperty(i, object); + } + return ret; +} + +PageRouterAttached::PageRouterAttached(QObject *parent) + : QObject(parent) + , m_preload(new PreloadRouteGroup(this)) +{ + findParent(); + auto item = qobject_cast(parent); + if (item != nullptr) { + connect(item, &QQuickItem::windowChanged, this, [this]() { + findParent(); + }); + connect(item, &QQuickItem::parentChanged, this, [this]() { + findParent(); + }); + } +} diff --git a/src/pagerouter.h b/src/pagerouter.h new file mode 100644 index 0000000..8fae273 --- /dev/null +++ b/src/pagerouter.h @@ -0,0 +1,811 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include "columnview.h" +#include +#include +#include +#include + +static std::map s_knownVariants; +class PageRouter; + +class ParsedRoute : public QObject +{ + Q_OBJECT + +public: + QString name; + QVariant data; + QVariantMap properties; + bool cache; + QQuickItem *item = nullptr; + void itemDestroyed() + { + item = nullptr; + } + QQuickItem *setItem(QQuickItem *newItem) + { + auto ret = item; + if (ret != nullptr) { + disconnect(ret, &QObject::destroyed, this, &ParsedRoute::itemDestroyed); + } + item = newItem; + if (newItem != nullptr) { + connect(newItem, &QObject::destroyed, this, &ParsedRoute::itemDestroyed); + } + return ret; + } + explicit ParsedRoute(const QString &name = QString(), + QVariant data = QVariant(), + QVariantMap properties = QVariantMap(), + bool cache = false, + QQuickItem *item = nullptr) + : name(name) + , data(data) + , properties(properties) + , cache(cache) + { + setItem(item); + } + ~ParsedRoute() override + { + if (item) { + item->deleteLater(); + } + } + quint32 hash() + { + for (auto i = s_knownVariants.begin(); i != s_knownVariants.end(); i++) { + if (i->second == data) { + return i->first; + } + } + auto number = QRandomGenerator::system()->generate(); + while (s_knownVariants.count(number) > 0) { + number = QRandomGenerator::system()->generate(); + } + s_knownVariants[number] = data; + return number; + } + bool equals(const ParsedRoute *rhs, bool countItem = false) + { + /* clang-format off */ + return name == rhs->name + && data == rhs->data + && (!countItem || item == rhs->item) + && cache == rhs->cache; + /* clang-format on */ + } +}; + +struct LRU { + int size = 10; + QList> evictionList; + QMap, int> costs; + QMap, ParsedRoute *> items; + + ParsedRoute *take(QPair key) + { + auto ret = items.take(key); + evictionList.removeAll(key); + return ret; + } + int totalCosts() + { + int ret = 0; + for (auto cost : std::as_const(costs)) { + ret += cost; + } + return ret; + } + void setSize(int size = 10) + { + this->size = size; + prune(); + } + void prune() + { + while (size < totalCosts()) { + auto key = evictionList.last(); + auto item = items.take(key); + delete item; + costs.take(key); + evictionList.takeLast(); + } + } + void insert(QPair key, ParsedRoute *newItem, int cost) + { + if (items.contains(key)) { + auto item = items.take(key); + evictionList.removeAll(key); + if (item != newItem) { + delete item; + } + } + costs[key] = cost; + items[key] = newItem; + evictionList.prepend(key); + prune(); + } +}; + +class PageRouterAttached; + +/** + * Item holding data about when to preload a route. + * + * @code{.qml} + * preload { + * route: "updates-page" + * when: updatesCount > 0 + * } + * @endcode + */ +class PreloadRouteGroup : public QObject +{ + Q_OBJECT + + /** + * @brief The route to preload. + * + * When the condition is false, the route will not be preloaded. + */ + Q_PROPERTY(QJSValue route MEMBER m_route WRITE setRoute NOTIFY changed) + QJSValue m_route; + + /** + * @brief When the route should be preloaded. + * + * When the condition is false, the route will not be preloaded. + */ + Q_PROPERTY(bool when MEMBER m_when NOTIFY changed) + bool m_when; + + void handleChange(); + PageRouterAttached *m_parent; + +public: + ~PreloadRouteGroup() override; + void setRoute(QJSValue route) + { + m_route = route; + Q_EMIT changed(); + } + PreloadRouteGroup(QObject *parent) + : QObject(parent) + { + m_parent = qobject_cast(parent); + Q_ASSERT(m_parent); + connect(this, &PreloadRouteGroup::changed, this, &PreloadRouteGroup::handleChange); + } + Q_SIGNAL void changed(); +}; + +/** + * Item representing a route the PageRouter can navigate to. + * + * @include PageRoute.qml + * + * @see PageRouter + */ +class PageRoute : public QObject +{ + Q_OBJECT + + /** + * @brief The name of this route. + * + * This name should be unique per PageRoute in a PageRouter. + * When two PageRoutes have the same name, the one listed first + * in the PageRouter will be used. + */ + Q_PROPERTY(QString name MEMBER m_name READ name) + + /** + * @brief The page component of this route. + * + * This should be an instance of Component with a Kirigami::Page inside + * of it. + */ + Q_PROPERTY(QQmlComponent *component MEMBER m_component READ component) + + /** + * @brief Whether pages generated by this route should be cached or not. + * + * This should be an instance of Component with a Kirigami::Page inside + * of it. + * + * This will not work: + * + * @include PageRouterCachePagesDont.qml + * + * This will work: + * + * @include PageRouterCachePagesDo.qml + * + */ + Q_PROPERTY(bool cache MEMBER m_cache READ cache) + + /** + * @brief How expensive this route is on memory. + * + * This affects caching, as the sum of costs of routes in the cache + * can never exceed the cache's cap. + */ + Q_PROPERTY(int cost MEMBER m_cost) + + Q_CLASSINFO("DefaultProperty", "component") + +Q_SIGNALS: + void preloadDataChanged(); + void preloadChanged(); + +private: + QString m_name; + QQmlComponent *m_component = nullptr; + bool m_cache = false; + int m_cost = 1; + +public: + QQmlComponent *component() + { + return m_component; + }; + QString name() + { + return m_name; + }; + bool cache() + { + return m_cache; + }; + int cost() + { + return m_cost; + }; +}; + +/** + * An item managing pages and data of a ColumnView using named routes. + * + *

+ * + * ## Using a PageRouter + * + * Applications typically manage their contents via elements called "pages" or "screens." + * In Kirigami, these are called @link org::kde::kirigami::Page Pages @endlink and are + * arranged in @link PageRoute routes @endlink using a PageRouter to manage them. The PageRouter + * manages a stack of @link org::kde::kirigami::Page Pages @endlink created from a pool of potential + * @link PageRoute PageRoutes @endlink. + * + * Unlike most traditional stacks, a PageRouter provides functions for random access to its pages + * with navigateToRoute and routeActive. + * + * When your user interface fits the stack paradigm and is likely to use random access navigation, + * using the PageRouter is appropriate. For simpler navigation, it is more appropriate to avoid + * the overhead of a PageRouter by using a @link org::kde::kirigami::PageRow PageRow @endlink + * instead. + * + *

+ * + * ## Navigation Model + * + * A PageRouter draws from a pool of @link PageRoute PageRoutes @endlink in order to construct + * its stack. + * + * @image html PageRouterModel.svg width=50% + * + *

+ * + * You can push pages onto this stack... + * + * @image html PageRouterPush.svg width=50% + * + * ...or pop them off... + * + * @image html PageRouterPop.svg width=50% + * + * ...or navigate to an arbitrary collection of pages. + * + * @image html PageRouterNavigate.svg width=50% + * + *

+ * + * Components are able to query the PageRouter about the currently active routes + * on the stack. This is useful for e.g. a card indicating that the page it takes + * the user to is currently active. + * + *

+ * + * ## Example + * + * @include PageRouter.qml + * + * @see PageRouterAttached + * @see PageRoute + */ +class PageRouter : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + /** + * @brief The named routes a PageRouter can navigate to. + * + * @include PageRouterRoutes.qml + */ + Q_PROPERTY(QQmlListProperty routes READ routes) + + Q_CLASSINFO("DefaultProperty", "routes") + + /** + * @brief The initial route. + * + * `initialRoute` is the page that the PageRouter will push upon + * creation. Changing it after creation will cause the PageRouter to reset + * its state. Not providing an `initialRoute` will result in undefined + * behavior. + * + * @see org::kde::kirigami::PageRoute::name + * @include PageRouterInitialRoute.qml + */ + Q_PROPERTY(QJSValue initialRoute READ initialRoute WRITE setInitialRoute NOTIFY initialRouteChanged) + + /** + * @brief The ColumnView being puppeted by the PageRouter. + * + * All PageRouters should be created with a ColumnView, and creating one without + * a ColumnView is undefined behaviour. + * + * @warning You should **not** directly interact with a ColumnView being puppeted + * by a PageRouter. Instead, use a PageRouter's functions to manipulate the + * ColumnView. + * + * @include PageRouterColumnView.qml + */ + Q_PROPERTY(ColumnView *pageStack MEMBER m_pageStack NOTIFY pageStackChanged) + + /** + * @brief How large the cache can be. + * + * The combined costs of cached routes will never exceed the cache capacity. + */ + Q_PROPERTY(int cacheCapacity READ cacheCapacity WRITE setCacheCapacity) + + /** + * @brief How large the preloaded pool can be. + * + * The combined costs of preloaded routes will never exceed the pool capacity. + */ + Q_PROPERTY(int preloadedPoolCapacity READ preloadedPoolCapacity WRITE setPreloadedPoolCapacity) + + /** + * Exposes the data of all pages on the stack, preferring pages on the top + * (e.g. most recently pushed) to pages pushed on the bottom (least recently + * pushed). + */ + Q_PROPERTY(QQmlPropertyMap *params READ params CONSTANT) + +private: + /** + * The map exposing to QML all the params of the stack. This is a + * QQmlPropertyMap to allow binding to param values. This *does* lack + * the ability to drop items, but the amount of all params in an app + * is overwhelmingly likely to be fixed, not dynamic. + */ + QSharedPointer m_paramMap; + + /** + * Reevaluate the properties of the param map by going through all of the + * routes on the stack to determine the topmost value for every parametre. + * + * Should be called for every time a route is pushed, popped, or modified. + */ + void reevaluateParamMapProperties(); + + /** + * @brief The routes the PageRouter is aware of. + * + * Generally, this should not be mutated from C++, only read. + */ + QList m_routes; + + /** + * @brief The PageRouter being puppeted. + * + * m_pageRow is the ColumnView this PageRouter puppets. + */ + ColumnView *m_pageStack = nullptr; + + /** + * @brief The route that the PageRouter will load on completion. + * + * m_initialRoute is the raw QJSValue from QML that will be + * parsed into a ParsedRoute struct on construction. + * Generally, this should not be mutated from C++, only read. + */ + QJSValue m_initialRoute; + + /** + * @brief The current routes pushed on the PageRow. + * + * Generally, the state of m_pageRow and m_currentRoutes + * should be kept in sync. Undesirable behaviour will result + * from desynchronisation of the two. + */ + QList m_currentRoutes; + + /** + * @brief Cached routes. + * + * An LRU cache of ParsedRoutes with items that were previously on the stack. + */ + LRU m_cache; + + /** @brief Preloaded routes. + * + * A LRU cache of ParsedRoutes with items that may be on the stack in the future, + * but were not on the stack before. + */ + LRU m_preload; + + /** + * @brief Helper function to push a route. + * + * This function has the shared logic between + * navigateToRoute and pushRoute. + */ + void push(ParsedRoute *route); + + /** + * @brief Helper function to access whether m_routes has a key. + * + * This function abstracts the QJSValue. + */ + bool routesContainsKey(const QString &key) const; + + /** + * @brief Helper function to access the component of a key for m_routes. + * + * The return value will be a nullptr if @p key does not exist in + * m_routes. + */ + QQmlComponent *routesValueForKey(const QString &key) const; + + /** + * @brief Helper function to access the cache status of a key for m_routes. + * + * The return value will be false if @p key does not exist in + * m_routes. + */ + bool routesCacheForKey(const QString &key) const; + + /** + * @brief Helper function to access the cost of a key for m_routes. + * + * The return value will be -1 if @p key does not exist in + * m_routes. + */ + int routesCostForKey(const QString &key) const; + + void preload(ParsedRoute *route); + void unpreload(ParsedRoute *route); + + void placeInCache(ParsedRoute *route); + + static void appendRoute(QQmlListProperty *list, PageRoute *); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + static int routeCount(QQmlListProperty *list); + static PageRoute *route(QQmlListProperty *list, int); +#else + static qsizetype routeCount(QQmlListProperty *list); + static PageRoute *route(QQmlListProperty *list, qsizetype); +#endif + static void clearRoutes(QQmlListProperty *list); + + QVariant dataFor(QObject *object); + bool isActive(QObject *object); + void pushFromObject(QObject *object, QJSValue route, bool replace = false); + + friend class PageRouterAttached; + friend class PreloadRouteGroup; + friend class ParsedRoute; + +protected: + void classBegin() override; + void componentComplete() override; + +public: + PageRouter(QQuickItem *parent = nullptr); + ~PageRouter() override; + + QQmlListProperty routes(); + + QQmlPropertyMap *params() + { + return m_paramMap.data(); + } + + QJSValue initialRoute() const; + void setInitialRoute(QJSValue initialRoute); + + int cacheCapacity() const + { + return m_cache.size; + }; + void setCacheCapacity(int size) + { + m_cache.setSize(size); + }; + + int preloadedPoolCapacity() const + { + return m_preload.size; + }; + void setPreloadedPoolCapacity(int size) + { + m_preload.setSize(size); + }; + + /** + * @brief Navigate to the given route. + * + * Calling `navigateToRoute` causes the PageRouter to replace currently + * active pages with the new route. + * + * @param route The given route for the PageRouter to navigate to. + * A route is an array of variants or a single item. A string item will be interpreted + * as a page without associated data. An object item will be interpreted + * as follows: + * @code{.js} + * { + * "route": "/home" // The named page of the route. + * "data": QtObject {} // The data to pass to the page. + * } + * @endcode + * Navigating to a route not defined in a PageRouter's routes is undefined + * behavior. + * + * @code{.qml} + * Button { + * text: "Login" + * onClicked: { + * Kirigami.PageRouter.navigateToRoute(["/home", "/login"]) + * } + * } + * @endcode + */ + Q_INVOKABLE void navigateToRoute(QJSValue route); + + /** + * @brief Check whether the current route is on the stack. + * + * `routeActive` will return true if the given route + * is on the stack. + * + * @param route The given route to check for. + * + * `routeActive` returns true for partial routes like + * the following: + * + * @code{.js} + * Kirigami.PageRouter.navigateToRoute(["/home", "/login", "/google"]) + * Kirigami.PageRouter.routeActive(["/home", "/login"]) // returns true + * @endcode + * + * This only works from the root page, e.g. the following will return false: + * @code{.js} + * Kirigami.PageRouter.navigateToRoute(["/home", "/login", "/google"]) + * Kirigami.PageRouter.routeActive(["/login", "/google"]) // returns false + * @endcode + */ + Q_INVOKABLE bool routeActive(QJSValue route); + + /** + * @brief Appends a route to the currently navigated route. + * + * Calling `pushRoute` will append the given @p route to the currently navigated + * routes. See navigateToRoute() if you want to replace the items currently on + * the PageRouter. + * + * @param route The given route to push. + * + * @code{.js} + * Kirigami.PageRouter.navigateToRoute(["/home", "/login"]) + * // The PageRouter is navigated to /home/login + * Kirigami.PageRouter.pushRoute("/google") + * // The PageRouter is navigated to /home/login/google + * @endcode + */ + Q_INVOKABLE void pushRoute(QJSValue route); + + /** + * @brief Pops the last page on the router. + * + * Calling `popRoute` will result in the last page on the router getting popped. + * You should not call this function when there is only one page on the router. + * + * @code{.js} + * Kirigami.PageRouter.navigateToRoute(["/home", "/login"]) + * // The PageRouter is navigated to /home/login + * Kirigami.PageRouter.popRoute() + * // The PageRouter is navigated to /home + * @endcode + */ + Q_INVOKABLE void popRoute(); + + /** + * @brief Shifts keyboard focus and view to a given index on the PageRouter's stack. + * + * @param view The view to bring to focus. If this is an integer index, the PageRouter will + * navigate to the given index. If it's a route specifier, the PageRouter will navigate + * to the first route matching it. + * + * Navigating to route by index: + * @code{.js} + * Kirigami.PageRouter.navigateToRoute(["/home", "/browse", "/apps", "/login"]) + * Kirigami.PageRouter.bringToView(1) + * @endcode + * + * Navigating to route by name: + * @code{.js} + * Kirigami.PageRouter.navigateToRoute(["/home", "/browse", "/apps", "/login"]) + * Kirigami.PageRouter.bringToView("/browse") + * @endcode + * + * Navigating to route by data: + * @code{.js} + * Kirigami.PageRouter.navigateToRoute([{"route": "/page", "data": "red"}, + * {"route": "/page", "data": "blue"}, + * {"route": "/page", "data": "green"}, + * {"route": "/page", "data": "yellow"}]) + * Kirigami.PageRouter.bringToView({"route": "/page", "data": "blue"}) + * @endcode + */ + Q_INVOKABLE void bringToView(QJSValue route); + + /** + * @brief Returns a QJSValue corresponding to the current pages on the stack. + * + * The returned value is in the same form as the input to navigateToRoute. + */ + Q_INVOKABLE QJSValue currentRoutes() const; + + static PageRouterAttached *qmlAttachedProperties(QObject *object); + +Q_SIGNALS: + void routesChanged(); + void initialRouteChanged(); + void pageStackChanged(); + void currentIndexChanged(); + void navigationChanged(); +}; + +/** + * Attached object allowing children of a PageRouter to access its functions + * without requiring the children to have the parent PageRouter's id. + * + * @see PageRouter + */ +class PageRouterAttached : public QObject +{ + Q_OBJECT + + Q_PROPERTY(PageRouter *router READ router WRITE setRouter NOTIFY routerChanged) + /** + * The data for the page this item belongs to. Accessing this property + * outside of a PageRouter will result in undefined behavior. + */ + Q_PROPERTY(QVariant data READ data MEMBER m_data NOTIFY dataChanged) + + /** + * Whether the page this item belongs to is the current index of the ColumnView. + * Accessing this property outside of a PageRouter will result in undefined behaviour. + */ + Q_PROPERTY(bool isCurrent READ isCurrent NOTIFY isCurrentChanged) + + /** + * Which route this PageRouterAttached should watch for. + * + * @include PageRouterWatchedRoute.qml + */ + Q_PROPERTY(QJSValue watchedRoute READ watchedRoute WRITE setWatchedRoute NOTIFY watchedRouteChanged) + + /** + * Route preloading settings. + */ + Q_PROPERTY(PreloadRouteGroup *preload READ preload) + + /** + * Whether the watchedRoute is currently active. + */ + Q_PROPERTY(bool watchedRouteActive READ watchedRouteActive NOTIFY navigationChanged) + +private: + explicit PageRouterAttached(QObject *parent = nullptr); + + QPointer m_router; + PreloadRouteGroup *m_preload; + QVariant m_data; + QJSValue m_watchedRoute; + + void findParent(); + + friend class PageRouter; + friend class PreloadRouteGroup; + friend class ParsedRoute; + +public: + PreloadRouteGroup *preload() const + { + return m_preload; + }; + PageRouter *router() const + { + return m_router; + }; + void setRouter(PageRouter *router) + { + m_router = router; + Q_EMIT routerChanged(); + } + QVariant data() const; + bool isCurrent() const; + /// @see PageRouter::navigateToRoute() + Q_INVOKABLE void navigateToRoute(QJSValue route); + /// @see PageRouter::routeActive() + Q_INVOKABLE bool routeActive(QJSValue route); + /// @see PageRouter::pushRoute() + Q_INVOKABLE void pushRoute(QJSValue route); + /// @see PageRouter::popRoute() + Q_INVOKABLE void popRoute(); + // @see PageRouter::bringToView() + Q_INVOKABLE void bringToView(QJSValue route); + /** + * @brief Push a route from this route on the stack. + * + * Replace the routes after the route this is invoked on + * with the provided @p route. + * + * For example, if you invoke this method on the second route + * in the PageRouter's stack, routes after the second + * route will be replaced with the provided routes. + */ + Q_INVOKABLE void pushFromHere(QJSValue route); + /** + * @brief Pop routes after this route on the stack. + * + * Pop the routes after the route this is invoked on with + * the provided @p route. + * + * For example, if you invoke this method on the second route + * in the PageRouter's stack, routes after the second route + * will be removed from the stack. + */ + Q_INVOKABLE void popFromHere(); + /** + * @brief Replaces this route with the given routes on the stack. + * + * Behaves like pushFromHere, except the current route is also + * popped. + */ + Q_INVOKABLE void replaceFromHere(QJSValue route); + bool watchedRouteActive(); + void setWatchedRoute(QJSValue route); + QJSValue watchedRoute(); + +Q_SIGNALS: + void routerChanged(); + void dataChanged(); + void isCurrentChanged(); + void navigationChanged(); + void watchedRouteChanged(); +}; + +QML_DECLARE_TYPEINFO(PageRouter, QML_HAS_ATTACHED_PROPERTIES) diff --git a/src/plugins.qmltypes b/src/plugins.qmltypes new file mode 100644 index 0000000..bbfca8c --- /dev/null +++ b/src/plugins.qmltypes @@ -0,0 +1,2158 @@ +import QtQuick.tooling 1.2 + +// This file describes the plugin-supplied types contained in the library. +// It is used for QML tooling purposes only. +// +// This file was auto-generated by: +// 'qmlplugindump -noinstantiate -notrelocatable org.kde.kirigami 2.0 /opt/kde5/lib/x86_64-linux-gnu/qml/' + +Module { + dependencies: [ + "QtGraphicalEffects 1.12", + "QtQml 2.7", + "QtQml.Models 2.2", + "QtQml.WorkerScript 2.15", + "QtQuick 2.9", + "QtQuick.Controls 2.5", + "QtQuick.Controls.Material 2.15", + "QtQuick.Controls.Styles 1.4", + "QtQuick.Controls.Styles.Plasma 2.0", + "QtQuick.Layouts 1.4", + "QtQuick.Templates 2.4", + "QtQuick.Window 2.6", + "org.kde.breeze 1.0", + "org.kde.kconfig 1.0", + "org.kde.kquickcontrolsaddons 2.0", + "org.kde.plasma.components 3.0", + "org.kde.plasma.core 2.0", + "org.kde.plasma.extras 2.0", + "org.kde.quickcharts 1.0", + "org.kde.quickcharts.controls 1.0" + ] + Component { + name: "ApplicationHeaderStyle" + prototype: "QObject" + exports: ["org.kde.kirigami/ApplicationHeaderStyle 2.0"] + isCreatable: false + exportMetaObjectRevisions: [0] + Enum { + name: "Status" + values: { + "Auto": 0, + "Breadcrumb": 1, + "Titles": 2, + "TabBar": 3, + "ToolBar": 4, + "None": 5 + } + } + Enum { + name: "NavigationButton" + values: { + "NoNavigationButtons": 0, + "ShowBackButton": 1, + "ShowForwardButton": 2 + } + } + } + Component { + name: "AvatarGroup" + prototype: "QObject" + exports: ["org.kde.kirigami.private/AvatarGroup 2.14"] + exportMetaObjectRevisions: [0] + Property { name: "main"; type: "QVariant" } + Property { name: "secondary"; type: "QVariant" } + Signal { name: "mainActionChanged" } + Signal { name: "secondaryActionChanged" } + } + Component { + name: "BorderGroup" + prototype: "QObject" + exports: ["org.kde.kirigami/BorderGroup 2.12"] + isCreatable: false + exportMetaObjectRevisions: [0] + Property { name: "width"; type: "double" } + Property { name: "color"; type: "QColor" } + Signal { name: "changed" } + } + Component { + name: "ColorUtils" + prototype: "QObject" + exports: ["org.kde.kirigami/ColorUtils 2.12"] + isCreatable: false + isSingleton: true + exportMetaObjectRevisions: [0] + Enum { + name: "Brightness" + values: { + "Dark": 0, + "Light": 1 + } + } + Method { + name: "brightnessForColor" + type: "ColorUtils::Brightness" + Parameter { name: "color"; type: "QColor" } + } + Method { + name: "grayForColor" + type: "double" + Parameter { name: "color"; type: "QColor" } + } + Method { + name: "alphaBlend" + type: "QColor" + Parameter { name: "foreground"; type: "QColor" } + Parameter { name: "background"; type: "QColor" } + } + Method { + name: "linearInterpolation" + type: "QColor" + Parameter { name: "one"; type: "QColor" } + Parameter { name: "two"; type: "QColor" } + Parameter { name: "balance"; type: "double" } + } + Method { + name: "adjustColor" + type: "QColor" + Parameter { name: "color"; type: "QColor" } + Parameter { name: "adjustments"; type: "QJSValue" } + } + Method { + name: "scaleColor" + type: "QColor" + Parameter { name: "color"; type: "QColor" } + Parameter { name: "adjustments"; type: "QJSValue" } + } + Method { + name: "tintWithAlpha" + type: "QColor" + Parameter { name: "targetColor"; type: "QColor" } + Parameter { name: "tintColor"; type: "QColor" } + Parameter { name: "alpha"; type: "double" } + } + Method { + name: "chroma" + type: "double" + Parameter { name: "color"; type: "QColor" } + } + } + Component { + name: "ColumnView" + defaultProperty: "contentData" + prototype: "QQuickItem" + exports: ["org.kde.kirigami/ColumnView 2.7"] + exportMetaObjectRevisions: [0] + attachedType: "ColumnViewAttached" + Enum { + name: "ColumnResizeMode" + values: { + "FixedColumns": 0, + "DynamicColumns": 1, + "SingleColumn": 2 + } + } + Property { name: "columnResizeMode"; type: "ColumnResizeMode" } + Property { name: "columnWidth"; type: "double" } + Property { name: "count"; type: "int"; isReadonly: true } + Property { name: "currentIndex"; type: "int" } + Property { name: "currentItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "contentItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "contentX"; type: "double" } + Property { name: "contentWidth"; type: "double"; isReadonly: true } + Property { name: "topPadding"; type: "double" } + Property { name: "bottomPadding"; type: "double" } + Property { name: "scrollDuration"; type: "int" } + Property { name: "separatorVisible"; type: "bool" } + Property { name: "visibleItems"; type: "QList"; isReadonly: true } + Property { name: "firstVisibleItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "lastVisibleItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "dragging"; type: "bool"; isReadonly: true } + Property { name: "moving"; type: "bool"; isReadonly: true } + Property { name: "interactive"; type: "bool" } + Property { name: "acceptsMouse"; type: "bool" } + Property { name: "contentChildren"; type: "QQuickItem"; isList: true; isReadonly: true } + Property { name: "contentData"; type: "QObject"; isList: true; isReadonly: true } + Signal { + name: "itemInserted" + Parameter { name: "position"; type: "int" } + Parameter { name: "item"; type: "QQuickItem"; isPointer: true } + } + Signal { + name: "itemRemoved" + Parameter { name: "item"; type: "QQuickItem"; isPointer: true } + } + Method { + name: "addItem" + Parameter { name: "item"; type: "QQuickItem"; isPointer: true } + } + Method { + name: "insertItem" + Parameter { name: "pos"; type: "int" } + Parameter { name: "item"; type: "QQuickItem"; isPointer: true } + } + Method { + name: "replaceItem" + Parameter { name: "pos"; type: "int" } + Parameter { name: "item"; type: "QQuickItem"; isPointer: true } + } + Method { + name: "moveItem" + Parameter { name: "from"; type: "int" } + Parameter { name: "to"; type: "int" } + } + Method { + name: "removeItem" + type: "QQuickItem*" + Parameter { name: "item"; type: "QVariant" } + } + Method { + name: "pop" + type: "QQuickItem*" + Parameter { name: "item"; type: "QQuickItem"; isPointer: true } + } + Method { name: "clear" } + Method { + name: "containsItem" + type: "bool" + Parameter { name: "item"; type: "QQuickItem"; isPointer: true } + } + Method { + name: "itemAt" + type: "QQuickItem*" + Parameter { name: "x"; type: "double" } + Parameter { name: "y"; type: "double" } + } + } + Component { + name: "ColumnViewAttached" + prototype: "QObject" + Property { name: "index"; type: "int" } + Property { name: "fillWidth"; type: "bool" } + Property { name: "reservedSpace"; type: "double" } + Property { name: "preventStealing"; type: "bool" } + Property { name: "pinned"; type: "bool" } + Property { name: "view"; type: "ColumnView"; isReadonly: true; isPointer: true } + Property { name: "inViewport"; type: "bool"; isReadonly: true } + Signal { + name: "scrollIntention" + Parameter { name: "event"; type: "ScrollIntentionEvent"; isPointer: true } + } + } + Component { + name: "CopyHelperPrivate" + prototype: "QObject" + exports: ["org.kde.kirigami.private/CopyHelperPrivate 2.6"] + isCreatable: false + isSingleton: true + exportMetaObjectRevisions: [0] + Method { + name: "copyTextToClipboard" + Parameter { name: "text"; type: "string" } + } + } + Component { + name: "CornersGroup" + prototype: "QObject" + exports: ["org.kde.kirigami/CornersGroup 2.12"] + isCreatable: false + exportMetaObjectRevisions: [0] + Property { name: "topLeftRadius"; type: "double" } + Property { name: "topRightRadius"; type: "double" } + Property { name: "bottomLeftRadius"; type: "double" } + Property { name: "bottomRightRadius"; type: "double" } + Signal { name: "changed" } + } + Component { + name: "DelegateRecycler" + defaultProperty: "data" + prototype: "QQuickItem" + exports: ["org.kde.kirigami/DelegateRecycler 2.4"] + exportMetaObjectRevisions: [0] + attachedType: "DelegateRecyclerAttached" + Property { name: "sourceComponent"; type: "QQmlComponent"; isPointer: true } + } + Component { + name: "DelegateRecyclerAttached" + prototype: "QObject" + Signal { name: "pooled" } + Signal { name: "reused" } + } + Component { + name: "DisplayHint" + prototype: "QObject" + exports: ["org.kde.kirigami/DisplayHint 2.14"] + isCreatable: false + isSingleton: true + exportMetaObjectRevisions: [0] + Enum { + name: "Hint" + values: { + "NoPreference": 0, + "IconOnly": 1, + "KeepVisible": 2, + "AlwaysHide": 4, + "HideChildIndicator": 8 + } + } + Enum { + name: "DisplayHints" + values: { + "NoPreference": 0, + "IconOnly": 1, + "KeepVisible": 2, + "AlwaysHide": 4, + "HideChildIndicator": 8 + } + } + Method { + name: "displayHintSet" + type: "bool" + Parameter { name: "values"; type: "DisplayHints" } + Parameter { name: "hint"; type: "Hint" } + } + Method { + name: "displayHintSet" + type: "bool" + Parameter { name: "object"; type: "QObject"; isPointer: true } + Parameter { name: "hint"; type: "Hint" } + } + } + Component { + name: "FormLayoutAttached" + prototype: "QObject" + exports: ["org.kde.kirigami/FormData 2.3"] + isCreatable: false + exportMetaObjectRevisions: [0] + Property { name: "label"; type: "string" } + Property { name: "labelAlignment"; type: "int" } + Property { name: "isSection"; type: "bool" } + Property { name: "checkable"; type: "bool" } + Property { name: "checked"; type: "bool" } + Property { name: "enabled"; type: "bool" } + Property { name: "buddyFor"; type: "QQuickItem"; isPointer: true } + } + Component { + name: "Icon" + defaultProperty: "data" + prototype: "QQuickItem" + exports: ["org.kde.kirigami/Icon 2.0"] + exportMetaObjectRevisions: [0] + Enum { + name: "Status" + values: { + "Null": 0, + "Ready": 1, + "Loading": 2, + "Error": 3 + } + } + Property { name: "source"; type: "QVariant" } + Property { name: "fallback"; type: "string" } + Property { name: "placeholder"; type: "string" } + Property { name: "active"; type: "bool" } + Property { name: "valid"; type: "bool"; isReadonly: true } + Property { name: "selected"; type: "bool" } + Property { name: "isMask"; type: "bool" } + Property { name: "color"; type: "QColor" } + Property { name: "status"; type: "Icon::Status"; isReadonly: true } + Property { name: "paintedWidth"; type: "double"; isReadonly: true } + Property { name: "paintedHeight"; type: "double"; isReadonly: true } + Signal { + name: "fallbackChanged" + Parameter { name: "fallback"; type: "string" } + } + Signal { + name: "placeholderChanged" + Parameter { name: "placeholder"; type: "string" } + } + Signal { name: "paintedAreaChanged" } + } + Component { + name: "ImageColors" + prototype: "QObject" + exports: ["org.kde.kirigami/ImageColors 2.13"] + exportMetaObjectRevisions: [0] + Property { name: "source"; type: "QVariant" } + Property { name: "palette"; type: "QVariantList"; isReadonly: true } + Property { name: "paletteBrightness"; type: "ColorUtils::Brightness"; isReadonly: true } + Property { name: "average"; type: "QColor"; isReadonly: true } + Property { name: "dominant"; type: "QColor"; isReadonly: true } + Property { name: "dominantContrast"; type: "QColor"; isReadonly: true } + Property { name: "highlight"; type: "QColor"; isReadonly: true } + Property { name: "foreground"; type: "QColor"; isReadonly: true } + Property { name: "background"; type: "QColor"; isReadonly: true } + Property { name: "closestToWhite"; type: "QColor"; isReadonly: true } + Property { name: "closestToBlack"; type: "QColor"; isReadonly: true } + Property { name: "fallbackPalette"; type: "QVariantList" } + Property { name: "fallbackPaletteBrightness"; type: "ColorUtils::Brightness" } + Property { name: "fallbackAverage"; type: "QColor" } + Property { name: "fallbackDominant"; type: "QColor" } + Property { name: "fallbackDominantContrasting"; type: "QColor" } + Property { name: "fallbackHighlight"; type: "QColor" } + Property { name: "fallbackForeground"; type: "QColor" } + Property { name: "fallbackBackground"; type: "QColor" } + Method { name: "update" } + } + Component { + name: "Kirigami::BasicThemeDefinition" + prototype: "QObject" + exports: [ + "org.kde.kirigami/BasicThemeDefinition 2.16", + "org.kde.kirigami/Theme 2.0" + ] + isCreatable: false + isSingleton: true + exportMetaObjectRevisions: [0, 0] + Property { name: "textColor"; type: "QColor" } + Property { name: "disabledTextColor"; type: "QColor" } + Property { name: "highlightColor"; type: "QColor" } + Property { name: "highlightedTextColor"; type: "QColor" } + Property { name: "backgroundColor"; type: "QColor" } + Property { name: "alternateBackgroundColor"; type: "QColor" } + Property { name: "focusColor"; type: "QColor" } + Property { name: "hoverColor"; type: "QColor" } + Property { name: "activeTextColor"; type: "QColor" } + Property { name: "activeBackgroundColor"; type: "QColor" } + Property { name: "linkColor"; type: "QColor" } + Property { name: "linkBackgroundColor"; type: "QColor" } + Property { name: "visitedLinkColor"; type: "QColor" } + Property { name: "visitedLinkBackgroundColor"; type: "QColor" } + Property { name: "negativeTextColor"; type: "QColor" } + Property { name: "negativeBackgroundColor"; type: "QColor" } + Property { name: "neutralTextColor"; type: "QColor" } + Property { name: "neutralBackgroundColor"; type: "QColor" } + Property { name: "positiveTextColor"; type: "QColor" } + Property { name: "positiveBackgroundColor"; type: "QColor" } + Property { name: "buttonTextColor"; type: "QColor" } + Property { name: "buttonBackgroundColor"; type: "QColor" } + Property { name: "buttonAlternateBackgroundColor"; type: "QColor" } + Property { name: "buttonHoverColor"; type: "QColor" } + Property { name: "buttonFocusColor"; type: "QColor" } + Property { name: "viewTextColor"; type: "QColor" } + Property { name: "viewBackgroundColor"; type: "QColor" } + Property { name: "viewAlternateBackgroundColor"; type: "QColor" } + Property { name: "viewHoverColor"; type: "QColor" } + Property { name: "viewFocusColor"; type: "QColor" } + Property { name: "selectionTextColor"; type: "QColor" } + Property { name: "selectionBackgroundColor"; type: "QColor" } + Property { name: "selectionAlternateBackgroundColor"; type: "QColor" } + Property { name: "selectionHoverColor"; type: "QColor" } + Property { name: "selectionFocusColor"; type: "QColor" } + Property { name: "tooltipTextColor"; type: "QColor" } + Property { name: "tooltipBackgroundColor"; type: "QColor" } + Property { name: "tooltipAlternateBackgroundColor"; type: "QColor" } + Property { name: "tooltipHoverColor"; type: "QColor" } + Property { name: "tooltipFocusColor"; type: "QColor" } + Property { name: "complementaryTextColor"; type: "QColor" } + Property { name: "complementaryBackgroundColor"; type: "QColor" } + Property { name: "complementaryAlternateBackgroundColor"; type: "QColor" } + Property { name: "complementaryHoverColor"; type: "QColor" } + Property { name: "complementaryFocusColor"; type: "QColor" } + Property { name: "headerTextColor"; type: "QColor" } + Property { name: "headerBackgroundColor"; type: "QColor" } + Property { name: "headerAlternateBackgroundColor"; type: "QColor" } + Property { name: "headerHoverColor"; type: "QColor" } + Property { name: "headerFocusColor"; type: "QColor" } + Property { name: "defaultFont"; type: "QFont" } + Property { name: "smallFont"; type: "QFont" } + Signal { name: "changed" } + Signal { + name: "sync" + Parameter { name: "object"; type: "QQuickItem"; isPointer: true } + } + } + Component { + name: "Kirigami::PlatformTheme" + prototype: "QObject" + exports: ["org.kde.kirigami/Theme 2.2"] + isCreatable: false + exportMetaObjectRevisions: [0] + Enum { + name: "ColorSet" + values: { + "View": 0, + "Window": 1, + "Button": 2, + "Selection": 3, + "Tooltip": 4, + "Complementary": 5, + "Header": 6, + "ColorSetCount": 7 + } + } + Enum { + name: "ColorGroup" + values: { + "Disabled": 1, + "Active": 0, + "Inactive": 2, + "Normal": 0, + "ColorGroupCount": 1 + } + } + Property { name: "colorSet"; type: "ColorSet" } + Property { name: "colorGroup"; type: "ColorGroup" } + Property { name: "inherit"; type: "bool" } + Property { name: "textColor"; type: "QColor" } + Property { name: "disabledTextColor"; type: "QColor" } + Property { name: "highlightedTextColor"; type: "QColor" } + Property { name: "activeTextColor"; type: "QColor" } + Property { name: "linkColor"; type: "QColor" } + Property { name: "visitedLinkColor"; type: "QColor" } + Property { name: "negativeTextColor"; type: "QColor" } + Property { name: "neutralTextColor"; type: "QColor" } + Property { name: "positiveTextColor"; type: "QColor" } + Property { name: "backgroundColor"; type: "QColor" } + Property { name: "alternateBackgroundColor"; type: "QColor" } + Property { name: "highlightColor"; type: "QColor" } + Property { name: "activeBackgroundColor"; type: "QColor" } + Property { name: "linkBackgroundColor"; type: "QColor" } + Property { name: "visitedLinkBackgroundColor"; type: "QColor" } + Property { name: "negativeBackgroundColor"; type: "QColor" } + Property { name: "neutralBackgroundColor"; type: "QColor" } + Property { name: "positiveBackgroundColor"; type: "QColor" } + Property { name: "focusColor"; type: "QColor" } + Property { name: "hoverColor"; type: "QColor" } + Property { name: "defaultFont"; type: "QFont"; isReadonly: true } + Property { name: "smallFont"; type: "QFont"; isReadonly: true } + Property { name: "palette"; type: "QPalette"; isReadonly: true } + Signal { name: "colorsChanged" } + Signal { + name: "defaultFontChanged" + Parameter { name: "font"; type: "QFont" } + } + Signal { + name: "smallFontChanged" + Parameter { name: "font"; type: "QFont" } + } + Signal { + name: "colorSetChanged" + Parameter { name: "colorSet"; type: "Kirigami::PlatformTheme::ColorSet" } + } + Signal { + name: "colorGroupChanged" + Parameter { name: "colorGroup"; type: "Kirigami::PlatformTheme::ColorGroup" } + } + Signal { + name: "paletteChanged" + Parameter { name: "pal"; type: "QPalette" } + } + Signal { + name: "inheritChanged" + Parameter { name: "inherit"; type: "bool" } + } + Method { + name: "iconFromTheme" + type: "QIcon" + Parameter { name: "name"; type: "string" } + Parameter { name: "customColor"; type: "QColor" } + } + Method { + name: "iconFromTheme" + type: "QIcon" + Parameter { name: "name"; type: "string" } + } + } + Component { + name: "KirigamiWheelEvent" + prototype: "QObject" + exports: ["org.kde.kirigami/WheelEvent 2.9"] + isCreatable: false + exportMetaObjectRevisions: [0] + Property { name: "x"; type: "double"; isReadonly: true } + Property { name: "y"; type: "double"; isReadonly: true } + Property { name: "angleDelta"; type: "QPointF"; isReadonly: true } + Property { name: "pixelDelta"; type: "QPointF"; isReadonly: true } + Property { name: "buttons"; type: "int"; isReadonly: true } + Property { name: "modifiers"; type: "int"; isReadonly: true } + Property { name: "inverted"; type: "bool"; isReadonly: true } + Property { name: "accepted"; type: "bool" } + } + Component { + name: "MessageType" + prototype: "QObject" + exports: ["org.kde.kirigami/MessageType 2.4"] + isCreatable: false + exportMetaObjectRevisions: [0] + Enum { + name: "Type" + values: { + "Information": 0, + "Positive": 1, + "Warning": 2, + "Error": 3 + } + } + } + Component { + name: "MnemonicAttached" + prototype: "QObject" + exports: ["org.kde.kirigami/MnemonicData 2.3"] + isCreatable: false + exportMetaObjectRevisions: [0] + Enum { + name: "ControlType" + values: { + "ActionElement": 0, + "DialogButton": 1, + "MenuItem": 2, + "FormLabel": 3, + "SecondaryControl": 4 + } + } + Property { name: "label"; type: "string" } + Property { name: "richTextLabel"; type: "string"; isReadonly: true } + Property { name: "mnemonicLabel"; type: "string"; isReadonly: true } + Property { name: "enabled"; type: "bool" } + Property { name: "controlType"; type: "MnemonicAttached::ControlType" } + Property { name: "sequence"; type: "QKeySequence"; isReadonly: true } + Property { name: "active"; type: "bool"; isReadonly: true } + } + Component { + name: "NameUtils" + prototype: "QObject" + exports: ["org.kde.kirigami/NameUtils 2.14"] + isCreatable: false + isSingleton: true + exportMetaObjectRevisions: [0] + Method { + name: "initialsFromString" + type: "string" + Parameter { name: "name"; type: "string" } + } + Method { + name: "colorsFromString" + type: "QColor" + Parameter { name: "name"; type: "string" } + } + Method { + name: "isStringUnsuitableForInitials" + type: "bool" + Parameter { name: "name"; type: "string" } + } + } + Component { + name: "PagePool" + prototype: "QObject" + exports: ["org.kde.kirigami/PagePool 2.11"] + exportMetaObjectRevisions: [0] + Property { name: "lastLoadedUrl"; type: "QUrl"; isReadonly: true } + Property { name: "lastLoadedItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "cachePages"; type: "bool" } + Method { + name: "loadPage" + type: "QQuickItem*" + Parameter { name: "url"; type: "string" } + Parameter { name: "callback"; type: "QJSValue" } + } + Method { + name: "loadPage" + type: "QQuickItem*" + Parameter { name: "url"; type: "string" } + } + Method { + name: "loadPageWithProperties" + type: "QQuickItem*" + Parameter { name: "url"; type: "string" } + Parameter { name: "properties"; type: "QVariantMap" } + Parameter { name: "callback"; type: "QJSValue" } + } + Method { + name: "loadPageWithProperties" + type: "QQuickItem*" + Parameter { name: "url"; type: "string" } + Parameter { name: "properties"; type: "QVariantMap" } + } + Method { + name: "urlForPage" + type: "QUrl" + Parameter { name: "item"; type: "QQuickItem"; isPointer: true } + } + Method { + name: "pageForUrl" + type: "QQuickItem*" + Parameter { name: "url"; type: "QUrl" } + } + Method { + name: "contains" + type: "bool" + Parameter { name: "page"; type: "QVariant" } + } + Method { + name: "deletePage" + Parameter { name: "page"; type: "QVariant" } + } + Method { + name: "resolvedUrl" + type: "QUrl" + Parameter { name: "file"; type: "string" } + } + Method { + name: "isLocalUrl" + type: "bool" + Parameter { name: "url"; type: "QUrl" } + } + Method { name: "clear" } + } + Component { + name: "PageRoute" + defaultProperty: "component" + prototype: "QObject" + exports: ["org.kde.kirigami/PageRoute 2.12"] + exportMetaObjectRevisions: [0] + Property { name: "name"; type: "string" } + Property { name: "component"; type: "QQmlComponent"; isPointer: true } + Property { name: "cache"; type: "bool" } + Property { name: "cost"; type: "int" } + Signal { name: "preloadDataChanged" } + Signal { name: "preloadChanged" } + } + Component { + name: "PageRouter" + defaultProperty: "routes" + prototype: "QObject" + exports: ["org.kde.kirigami/PageRouter 2.12"] + exportMetaObjectRevisions: [0] + attachedType: "PageRouterAttached" + Property { name: "routes"; type: "PageRoute"; isList: true; isReadonly: true } + Property { name: "initialRoute"; type: "QJSValue" } + Property { name: "pageStack"; type: "ColumnView"; isPointer: true } + Property { name: "cacheCapacity"; type: "int" } + Property { name: "preloadedPoolCapacity"; type: "int" } + Property { name: "params"; type: "QQmlPropertyMap"; isReadonly: true; isPointer: true } + Signal { name: "currentIndexChanged" } + Signal { name: "navigationChanged" } + Method { + name: "navigateToRoute" + Parameter { name: "route"; type: "QJSValue" } + } + Method { + name: "routeActive" + type: "bool" + Parameter { name: "route"; type: "QJSValue" } + } + Method { + name: "pushRoute" + Parameter { name: "route"; type: "QJSValue" } + } + Method { name: "popRoute" } + Method { + name: "bringToView" + Parameter { name: "route"; type: "QJSValue" } + } + Method { name: "currentRoutes"; type: "QJSValue" } + } + Component { + name: "PageRouterAttached" + prototype: "QObject" + exports: ["org.kde.kirigami/PageRouterAttached 2.12"] + isCreatable: false + exportMetaObjectRevisions: [0] + Property { name: "router"; type: "PageRouter"; isPointer: true } + Property { name: "data"; type: "QVariant" } + Property { name: "isCurrent"; type: "bool"; isReadonly: true } + Property { name: "watchedRoute"; type: "QJSValue" } + Property { name: "preload"; type: "PreloadRouteGroup"; isReadonly: true; isPointer: true } + Property { name: "watchedRouteActive"; type: "bool"; isReadonly: true } + Signal { name: "navigationChanged" } + Method { + name: "navigateToRoute" + Parameter { name: "route"; type: "QJSValue" } + } + Method { + name: "routeActive" + type: "bool" + Parameter { name: "route"; type: "QJSValue" } + } + Method { + name: "pushRoute" + Parameter { name: "route"; type: "QJSValue" } + } + Method { name: "popRoute" } + Method { + name: "bringToView" + Parameter { name: "route"; type: "QJSValue" } + } + Method { + name: "pushFromHere" + Parameter { name: "route"; type: "QJSValue" } + } + Method { name: "popFromHere" } + Method { + name: "replaceFromHere" + Parameter { name: "route"; type: "QJSValue" } + } + } + Component { + name: "PreloadRouteGroup" + prototype: "QObject" + exports: ["org.kde.kirigami/PreloadRouteGroup 2.14"] + isCreatable: false + exportMetaObjectRevisions: [0] + Property { name: "route"; type: "QJSValue" } + Property { name: "when"; type: "bool" } + Signal { name: "changed" } + } + Component { + name: "ScenePositionAttached" + prototype: "QObject" + exports: ["org.kde.kirigami/ScenePosition 2.5"] + isCreatable: false + exportMetaObjectRevisions: [0] + Property { name: "x"; type: "int"; isReadonly: true } + Property { name: "y"; type: "int"; isReadonly: true } + } + Component { + name: "Settings" + prototype: "QObject" + exports: ["org.kde.kirigami/Settings 2.0"] + isCreatable: false + isSingleton: true + exportMetaObjectRevisions: [0] + Property { name: "tabletModeAvailable"; type: "bool"; isReadonly: true } + Property { name: "isMobile"; type: "bool"; isReadonly: true } + Property { name: "tabletMode"; type: "bool"; isReadonly: true } + Property { name: "hasTransientTouchInput"; type: "bool"; isReadonly: true } + Property { name: "style"; type: "string"; isReadonly: true } + Property { name: "mouseWheelScrollLines"; type: "int"; isReadonly: true } + Property { name: "information"; type: "QStringList"; isReadonly: true } + Property { name: "applicationWindowIcon"; type: "QVariant"; isReadonly: true } + } + Component { + name: "ShadowGroup" + prototype: "QObject" + exports: ["org.kde.kirigami/ShadowGroup 2.12"] + isCreatable: false + exportMetaObjectRevisions: [0] + Property { name: "size"; type: "double" } + Property { name: "xOffset"; type: "double" } + Property { name: "yOffset"; type: "double" } + Property { name: "color"; type: "QColor" } + Signal { name: "changed" } + } + Component { + name: "ShadowedRectangle" + defaultProperty: "data" + prototype: "QQuickItem" + exports: ["org.kde.kirigami/ShadowedRectangle 2.12"] + exportMetaObjectRevisions: [0] + Property { name: "radius"; type: "double" } + Property { name: "color"; type: "QColor" } + Property { name: "border"; type: "BorderGroup"; isReadonly: true; isPointer: true } + Property { name: "shadow"; type: "ShadowGroup"; isReadonly: true; isPointer: true } + Property { name: "corners"; type: "CornersGroup"; isReadonly: true; isPointer: true } + Property { name: "softwareRendering"; type: "bool"; isReadonly: true } + } + Component { + name: "ShadowedTexture" + defaultProperty: "data" + prototype: "ShadowedRectangle" + exports: ["org.kde.kirigami/ShadowedTexture 2.12"] + exportMetaObjectRevisions: [0] + Property { name: "source"; type: "QQuickItem"; isPointer: true } + } + Component { + name: "SizeGroup" + prototype: "QObject" + exports: ["org.kde.kirigami/SizeGroup 2.14"] + exportMetaObjectRevisions: [0] + Enum { + name: "Mode" + values: { + "None": 0, + "Width": 1, + "Height": 2, + "Both": 3 + } + } + Property { name: "mode"; type: "Mode" } + Property { name: "items"; type: "QQuickItem"; isList: true; isReadonly: true } + Method { name: "relayout" } + } + Component { + name: "ToolBarLayout" + defaultProperty: "data" + prototype: "QQuickItem" + exports: ["org.kde.kirigami/ToolBarLayout 2.14"] + exportMetaObjectRevisions: [0] + attachedType: "ToolBarLayoutAttached" + Enum { + name: "HeightMode" + values: { + "AlwaysCenter": 0, + "AlwaysFill": 1, + "ConstrainIfLarger": 2 + } + } + Property { name: "actions"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "hiddenActions"; type: "QList"; isReadonly: true } + Property { name: "fullDelegate"; type: "QQmlComponent"; isPointer: true } + Property { name: "iconDelegate"; type: "QQmlComponent"; isPointer: true } + Property { name: "moreButton"; type: "QQmlComponent"; isPointer: true } + Property { name: "spacing"; type: "double" } + Property { name: "alignment"; type: "Qt::Alignment" } + Property { name: "visibleWidth"; type: "double"; isReadonly: true } + Property { name: "minimumWidth"; type: "double"; isReadonly: true } + Property { name: "layoutDirection"; type: "Qt::LayoutDirection" } + Property { name: "heightMode"; type: "HeightMode" } + Method { name: "relayout" } + } + Component { + name: "ToolBarLayoutAttached" + prototype: "QObject" + Property { name: "action"; type: "QObject"; isReadonly: true; isPointer: true } + } + Component { + name: "WheelHandler" + prototype: "QObject" + exports: ["org.kde.kirigami/WheelHandler 2.9"] + exportMetaObjectRevisions: [0] + Property { name: "target"; type: "QQuickItem"; isPointer: true } + Property { name: "blockTargetWheel"; type: "bool" } + Property { name: "scrollFlickableTarget"; type: "bool" } + Signal { + name: "wheel" + Parameter { name: "wheel"; type: "KirigamiWheelEvent"; isPointer: true } + } + } + Component { + prototype: "QQuickPage" + name: "org.kde.kirigami/AboutPage 2.6" + exports: ["org.kde.kirigami/AboutPage 2.6"] + exportMetaObjectRevisions: [6] + isComposite: true + defaultProperty: "mainItem" + Property { name: "aboutData"; type: "QVariant" } + Property { name: "mainItem"; type: "QObject"; isPointer: true } + Property { name: "keyboardNavigationEnabled"; type: "bool" } + Property { name: "refreshing"; type: "bool" } + Property { name: "supportsRefreshing"; type: "bool" } + Property { name: "flickable"; type: "QQuickFlickable"; isPointer: true } + Property { name: "verticalScrollBarPolicy"; type: "int" } + Property { name: "horizontalScrollBarPolicy"; type: "int" } + Property { name: "isCurrentPage"; type: "bool"; isReadonly: true } + Property { name: "icon"; type: "ActionIconGroup_QMLTYPE_29"; isPointer: true } + Property { name: "needsAttention"; type: "bool" } + Property { name: "progress"; type: "QVariant" } + Property { name: "titleDelegate"; type: "QQmlComponent"; isPointer: true } + Property { name: "globalToolBarItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "globalToolBarStyle"; type: "int" } + Property { name: "contextualActions"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "mainAction"; type: "QObject"; isPointer: true } + Property { name: "leftAction"; type: "QObject"; isPointer: true } + Property { name: "rightAction"; type: "QObject"; isPointer: true } + Property { + name: "actions" + type: "PageActionPropertyGroup_QMLTYPE_30" + isReadonly: true + isPointer: true + } + Property { name: "overlay"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Signal { name: "contextualActionsAboutToShow" } + Signal { + name: "backRequested" + Parameter { name: "event"; type: "QVariant" } + } + } + Component { + prototype: "QQuickPage" + name: "org.kde.kirigami/CategorizedSettings 2.18" + exports: ["org.kde.kirigami/CategorizedSettings 2.18"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "mainItem" + Property { name: "mainItem"; type: "QObject"; isPointer: true } + Property { name: "keyboardNavigationEnabled"; type: "bool" } + Property { name: "refreshing"; type: "bool" } + Property { name: "supportsRefreshing"; type: "bool" } + Property { name: "flickable"; type: "QQuickFlickable"; isPointer: true } + Property { name: "verticalScrollBarPolicy"; type: "int" } + Property { name: "horizontalScrollBarPolicy"; type: "int" } + Property { name: "isCurrentPage"; type: "bool"; isReadonly: true } + Property { name: "icon"; type: "ActionIconGroup_QMLTYPE_29"; isPointer: true } + Property { name: "needsAttention"; type: "bool" } + Property { name: "progress"; type: "QVariant" } + Property { name: "titleDelegate"; type: "QQmlComponent"; isPointer: true } + Property { name: "globalToolBarItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "globalToolBarStyle"; type: "int" } + Property { name: "contextualActions"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "mainAction"; type: "QObject"; isPointer: true } + Property { name: "leftAction"; type: "QObject"; isPointer: true } + Property { name: "rightAction"; type: "QObject"; isPointer: true } + Property { + name: "actions" + type: "PageActionPropertyGroup_QMLTYPE_30" + isReadonly: true + isPointer: true + } + Property { name: "overlay"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Signal { name: "contextualActionsAboutToShow" } + Signal { + name: "backRequested" + Parameter { name: "event"; type: "QVariant" } + } + } + Component { + prototype: "QQuickPage" + name: "org.kde.kirigami/SettingAction 2.18" + exports: ["org.kde.kirigami/SettingAction 2.18"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "mainItem" + Property { name: "mainItem"; type: "QObject"; isPointer: true } + Property { name: "keyboardNavigationEnabled"; type: "bool" } + Property { name: "refreshing"; type: "bool" } + Property { name: "supportsRefreshing"; type: "bool" } + Property { name: "flickable"; type: "QQuickFlickable"; isPointer: true } + Property { name: "verticalScrollBarPolicy"; type: "int" } + Property { name: "horizontalScrollBarPolicy"; type: "int" } + Property { name: "isCurrentPage"; type: "bool"; isReadonly: true } + Property { name: "icon"; type: "ActionIconGroup_QMLTYPE_29"; isPointer: true } + Property { name: "needsAttention"; type: "bool" } + Property { name: "progress"; type: "QVariant" } + Property { name: "titleDelegate"; type: "QQmlComponent"; isPointer: true } + Property { name: "globalToolBarItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "globalToolBarStyle"; type: "int" } + Property { name: "contextualActions"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "mainAction"; type: "QObject"; isPointer: true } + Property { name: "leftAction"; type: "QObject"; isPointer: true } + Property { name: "rightAction"; type: "QObject"; isPointer: true } + Property { + name: "actions" + type: "PageActionPropertyGroup_QMLTYPE_30" + isReadonly: true + isPointer: true + } + Property { name: "overlay"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Signal { name: "contextualActionsAboutToShow" } + Signal { + name: "backRequested" + Parameter { name: "event"; type: "QVariant" } + } + } + Component { + prototype: "QQuickItem" + name: "org.kde.kirigami/AbstractApplicationHeader 2.0" + exports: ["org.kde.kirigami/AbstractApplicationHeader 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "contentItem" + Property { name: "minimumHeight"; type: "int" } + Property { name: "preferredHeight"; type: "int" } + Property { name: "maximumHeight"; type: "int" } + Property { name: "position"; type: "int" } + Property { name: "pageRow"; type: "PageRow_QMLTYPE_23"; isPointer: true } + Property { name: "page"; type: "Page_QMLTYPE_32"; isPointer: true } + Property { name: "paintedHeight"; type: "int"; isReadonly: true } + Property { name: "leftPadding"; type: "int" } + Property { name: "topPadding"; type: "int" } + Property { name: "rightPadding"; type: "int" } + Property { name: "bottomPadding"; type: "int" } + Property { name: "separatorVisible"; type: "bool" } + Property { name: "__appWindow"; type: "QObject"; isPointer: true } + Property { name: "background"; type: "QQuickItem"; isPointer: true } + Property { name: "contentItem"; type: "QObject"; isList: true; isReadonly: true } + } + Component { + prototype: "QQuickItem" + name: "org.kde.kirigami/AbstractApplicationItem 2.1" + exports: ["org.kde.kirigami/AbstractApplicationItem 2.1"] + exportMetaObjectRevisions: [1] + isComposite: true + defaultProperty: "__data" + Property { name: "pageStack"; type: "QQuickItem"; isPointer: true } + Property { name: "activeFocusItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "font"; type: "QFont" } + Property { name: "palette"; type: "QVariant" } + Property { name: "locale"; type: "QQmlLocale"; isPointer: true } + Property { name: "menuBar"; type: "QQuickItem"; isPointer: true } + Property { name: "header"; type: "QQuickItem"; isPointer: true } + Property { name: "footer"; type: "QQuickItem"; isPointer: true } + Property { name: "controlsVisible"; type: "bool" } + Property { name: "globalDrawer"; type: "OverlayDrawer_QMLTYPE_113"; isPointer: true } + Property { name: "wideScreen"; type: "bool" } + Property { name: "contextDrawer"; type: "OverlayDrawer_QMLTYPE_113"; isPointer: true } + Property { name: "reachableMode"; type: "bool" } + Property { name: "reachableModeEnabled"; type: "bool" } + Property { name: "contentItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "color"; type: "QColor" } + Property { name: "background"; type: "QQuickItem"; isPointer: true } + Property { name: "overlay"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "__data"; type: "QObject"; isList: true; isReadonly: true } + Method { + name: "showPassiveNotification" + type: "QVariant" + Parameter { name: "message"; type: "QVariant" } + Parameter { name: "timeout"; type: "QVariant" } + Parameter { name: "actionText"; type: "QVariant" } + Parameter { name: "callBack"; type: "QVariant" } + } + Method { name: "hidePassiveNotification"; type: "QVariant" } + Method { name: "applicationWindow"; type: "QVariant" } + } + Component { + prototype: "QQuickApplicationWindow" + name: "org.kde.kirigami/AbstractApplicationWindow 2.0" + exports: ["org.kde.kirigami/AbstractApplicationWindow 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "contentData" + Property { name: "pageStack"; type: "QQuickItem"; isPointer: true } + Property { name: "controlsVisible"; type: "bool" } + Property { name: "globalDrawer"; type: "OverlayDrawer_QMLTYPE_113"; isPointer: true } + Property { name: "wideScreen"; type: "bool" } + Property { name: "contextDrawer"; type: "OverlayDrawer_QMLTYPE_113"; isPointer: true } + Property { name: "reachableMode"; type: "bool" } + Property { name: "reachableModeEnabled"; type: "bool" } + Property { name: "quitAction"; type: "Action_QMLTYPE_13"; isReadonly: true; isPointer: true } + Method { + name: "showPassiveNotification" + type: "QVariant" + Parameter { name: "message"; type: "QVariant" } + Parameter { name: "timeout"; type: "QVariant" } + Parameter { name: "actionText"; type: "QVariant" } + Parameter { name: "callBack"; type: "QVariant" } + } + Method { name: "hidePassiveNotification"; type: "QVariant" } + Method { name: "applicationWindow"; type: "QVariant" } + } + Component { + prototype: "QQuickItemDelegate" + name: "org.kde.kirigami/AbstractCard 2.4" + exports: ["org.kde.kirigami/AbstractCard 2.4"] + exportMetaObjectRevisions: [4] + isComposite: true + defaultProperty: "data" + Property { name: "header"; type: "QQuickItem"; isPointer: true } + Property { name: "headerOrientation"; type: "int" } + Property { name: "footer"; type: "QQuickItem"; isPointer: true } + Property { name: "showClickFeedback"; type: "bool" } + } + Component { + prototype: "QQuickControl" + name: "org.kde.kirigami/AbstractItemViewHeader 2.1" + exports: ["org.kde.kirigami/AbstractItemViewHeader 2.1"] + exportMetaObjectRevisions: [1] + isComposite: true + defaultProperty: "data" + Property { name: "minimumHeight"; type: "int" } + Property { name: "maximumHeight"; type: "int" } + Property { name: "view"; type: "QQuickListView"; isPointer: true } + } + Component { + prototype: "QQuickItemDelegate" + name: "org.kde.kirigami/AbstractListItem 2.0" + exports: ["org.kde.kirigami/AbstractListItem 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "_default" + Property { name: "supportsMouseEvents"; type: "bool" } + Property { name: "alternatingBackground"; type: "bool" } + Property { name: "sectionDelegate"; type: "bool" } + Property { name: "separatorVisible"; type: "bool" } + Property { name: "textColor"; type: "QColor" } + Property { name: "backgroundColor"; type: "QColor" } + Property { name: "alternateBackgroundColor"; type: "QColor" } + Property { name: "activeTextColor"; type: "QColor" } + Property { name: "activeBackgroundColor"; type: "QColor" } + Property { name: "action"; type: "QQuickAction"; isPointer: true } + Property { name: "containsMouse"; type: "bool"; isReadonly: true } + Property { name: "_default"; type: "QQuickItem"; isPointer: true } + } + Component { + prototype: "QQuickAction" + name: "org.kde.kirigami/Action 2.0" + exports: ["org.kde.kirigami/Action 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "children" + Enum { + name: "DisplayHint" + values: { + "NoPreference": 0, + "IconOnly": 1, + "KeepVisible": 2, + "AlwaysHide": 4, + "HideChildIndicator": 8 + } + } + Property { name: "visible"; type: "bool" } + Property { name: "tooltip"; type: "string" } + Property { name: "separator"; type: "bool" } + Property { name: "expandible"; type: "bool" } + Property { name: "parent"; type: "Action_QMLTYPE_12"; isPointer: true } + Property { name: "displayHint"; type: "int" } + Property { name: "displayComponent"; type: "QQmlComponent"; isPointer: true } + Property { name: "__children"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "visibleChildren"; type: "QVariant"; isReadonly: true } + Property { name: "iconName"; type: "string" } + Property { name: "iconSource"; type: "QUrl" } + Property { name: "children"; type: "QObject"; isList: true; isReadonly: true } + Method { + name: "displayHintSet" + type: "QVariant" + Parameter { name: "hint"; type: "QVariant" } + } + } + Component { + prototype: "QQuickTextField" + name: "org.kde.kirigami/ActionTextField 2.7" + exports: ["org.kde.kirigami/ActionTextField 2.7"] + exportMetaObjectRevisions: [7] + isComposite: true + defaultProperty: "data" + Property { name: "focusSequence"; type: "string" } + Property { name: "leftActions"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "rightActions"; type: "QObject"; isList: true; isReadonly: true } + } + Component { + prototype: "QQuickControl" + name: "org.kde.kirigami/ActionToolBar 2.5" + exports: ["org.kde.kirigami/ActionToolBar 2.5"] + exportMetaObjectRevisions: [5] + isComposite: true + defaultProperty: "data" + Property { name: "hiddenActions"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "flat"; type: "bool" } + Property { name: "display"; type: "int" } + Property { name: "position"; type: "int" } + Property { name: "overflowIconName"; type: "string" } + Property { name: "actions"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "alignment"; type: "int" } + Property { name: "maximumContentWidth"; type: "double"; isReadonly: true } + Property { name: "visibleWidth"; type: "double"; isReadonly: true } + Property { name: "heightMode"; type: "int" } + } + Component { + prototype: "QQuickItem" + name: "org.kde.kirigami/ApplicationHeader 2.0" + exports: ["org.kde.kirigami/ApplicationHeader 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "contentItem" + Property { name: "headerStyle"; type: "int" } + Property { name: "backButtonEnabled"; type: "bool" } + Property { name: "pageDelegate"; type: "QQmlComponent"; isPointer: true } + Property { name: "minimumHeight"; type: "int" } + Property { name: "preferredHeight"; type: "int" } + Property { name: "maximumHeight"; type: "int" } + Property { name: "position"; type: "int" } + Property { name: "pageRow"; type: "PageRow_QMLTYPE_23"; isPointer: true } + Property { name: "page"; type: "Page_QMLTYPE_32"; isPointer: true } + Property { name: "paintedHeight"; type: "int"; isReadonly: true } + Property { name: "leftPadding"; type: "int" } + Property { name: "topPadding"; type: "int" } + Property { name: "rightPadding"; type: "int" } + Property { name: "bottomPadding"; type: "int" } + Property { name: "separatorVisible"; type: "bool" } + Property { name: "__appWindow"; type: "QObject"; isPointer: true } + Property { name: "background"; type: "QQuickItem"; isPointer: true } + Property { name: "contentItem"; type: "QObject"; isList: true; isReadonly: true } + } + Component { + prototype: "QQuickItem" + name: "org.kde.kirigami/ApplicationItem 2.1" + exports: ["org.kde.kirigami/ApplicationItem 2.1"] + exportMetaObjectRevisions: [1] + isComposite: true + defaultProperty: "__data" + Property { name: "pageStack"; type: "PageRow_QMLTYPE_23"; isReadonly: true; isPointer: true } + Property { name: "activeFocusItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "font"; type: "QFont" } + Property { name: "palette"; type: "QVariant" } + Property { name: "locale"; type: "QQmlLocale"; isPointer: true } + Property { name: "menuBar"; type: "QQuickItem"; isPointer: true } + Property { name: "header"; type: "QQuickItem"; isPointer: true } + Property { name: "footer"; type: "QQuickItem"; isPointer: true } + Property { name: "controlsVisible"; type: "bool" } + Property { name: "globalDrawer"; type: "OverlayDrawer_QMLTYPE_113"; isPointer: true } + Property { name: "wideScreen"; type: "bool" } + Property { name: "contextDrawer"; type: "OverlayDrawer_QMLTYPE_113"; isPointer: true } + Property { name: "reachableMode"; type: "bool" } + Property { name: "reachableModeEnabled"; type: "bool" } + Property { name: "contentItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "color"; type: "QColor" } + Property { name: "background"; type: "QQuickItem"; isPointer: true } + Property { name: "overlay"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "__data"; type: "QObject"; isList: true; isReadonly: true } + Method { + name: "showPassiveNotification" + type: "QVariant" + Parameter { name: "message"; type: "QVariant" } + Parameter { name: "timeout"; type: "QVariant" } + Parameter { name: "actionText"; type: "QVariant" } + Parameter { name: "callBack"; type: "QVariant" } + } + Method { name: "hidePassiveNotification"; type: "QVariant" } + Method { name: "applicationWindow"; type: "QVariant" } + } + Component { + prototype: "QQuickApplicationWindow" + name: "org.kde.kirigami/ApplicationWindow 2.0" + exports: ["org.kde.kirigami/ApplicationWindow 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "contentData" + Property { name: "pageStack"; type: "PageRow_QMLTYPE_23"; isReadonly: true; isPointer: true } + Property { name: "controlsVisible"; type: "bool" } + Property { name: "globalDrawer"; type: "OverlayDrawer_QMLTYPE_113"; isPointer: true } + Property { name: "wideScreen"; type: "bool" } + Property { name: "contextDrawer"; type: "OverlayDrawer_QMLTYPE_113"; isPointer: true } + Property { name: "reachableMode"; type: "bool" } + Property { name: "reachableModeEnabled"; type: "bool" } + Property { name: "quitAction"; type: "Action_QMLTYPE_13"; isReadonly: true; isPointer: true } + Method { + name: "showPassiveNotification" + type: "QVariant" + Parameter { name: "message"; type: "QVariant" } + Parameter { name: "timeout"; type: "QVariant" } + Parameter { name: "actionText"; type: "QVariant" } + Parameter { name: "callBack"; type: "QVariant" } + } + Method { name: "hidePassiveNotification"; type: "QVariant" } + Method { name: "applicationWindow"; type: "QVariant" } + } + Component { + prototype: "QQuickControl" + name: "org.kde.kirigami/Avatar 2.13" + exports: ["org.kde.kirigami/Avatar 2.13"] + exportMetaObjectRevisions: [13] + isComposite: true + defaultProperty: "data" + Enum { + name: "ImageMode" + values: { + "AlwaysShowImage": 0, + "AdaptiveImageOrInitals": 1, + "AlwaysShowInitials": 2 + } + } + Enum { + name: "InitialsMode" + values: { + "UseInitials": 0, + "UseIcon": 1 + } + } + Property { name: "name"; type: "string" } + Property { name: "initialsMode"; type: "int" } + Property { name: "imageMode"; type: "int" } + Property { name: "color"; type: "QVariant" } + Property { name: "actions"; type: "AvatarGroup"; isPointer: true } + Property { name: "border"; type: "BorderPropertiesGroup_QMLTYPE_190"; isPointer: true } + Property { name: "source"; type: "QUrl" } + Property { name: "cache"; type: "bool" } + Property { name: "sourceSize"; type: "QSize" } + Property { name: "smooth"; type: "bool" } + } + Component { + prototype: "QQuickItemDelegate" + name: "org.kde.kirigami/BasicListItem 2.0" + exports: ["org.kde.kirigami/BasicListItem 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "_basicDefault" + Property { name: "leading"; type: "QQuickItem"; isPointer: true } + Property { name: "leadingPadding"; type: "double" } + Property { name: "trailing"; type: "QQuickItem"; isPointer: true } + Property { name: "trailingPadding"; type: "double" } + Property { name: "bold"; type: "bool" } + Property { name: "icon"; type: "QVariant" } + Property { name: "reserveSpaceForSubtitle"; type: "bool" } + Property { name: "fadeContent"; type: "bool" } + Property { name: "label"; type: "string" } + Property { name: "subtitle"; type: "string" } + Property { name: "iconSize"; type: "int" } + Property { name: "iconColor"; type: "QColor" } + Property { name: "reserveSpaceForIcon"; type: "bool" } + Property { name: "reserveSpaceForLabel"; type: "bool" } + Property { name: "textSpacing"; type: "double" } + Property { name: "_basicDefault"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "supportsMouseEvents"; type: "bool" } + Property { name: "alternatingBackground"; type: "bool" } + Property { name: "sectionDelegate"; type: "bool" } + Property { name: "separatorVisible"; type: "bool" } + Property { name: "textColor"; type: "QColor" } + Property { name: "backgroundColor"; type: "QColor" } + Property { name: "alternateBackgroundColor"; type: "QColor" } + Property { name: "activeTextColor"; type: "QColor" } + Property { name: "activeBackgroundColor"; type: "QColor" } + Property { name: "action"; type: "QQuickAction"; isPointer: true } + Property { name: "containsMouse"; type: "bool"; isReadonly: true } + Property { name: "_default"; type: "QQuickItem"; isPointer: true } + } + Component { + prototype: "QQuickItemDelegate" + name: "org.kde.kirigami/Card 2.4" + exports: ["org.kde.kirigami/Card 2.4"] + exportMetaObjectRevisions: [4] + isComposite: true + defaultProperty: "data" + Property { name: "actions"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "hiddenActions"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "banner"; type: "BannerImage_QMLTYPE_200"; isReadonly: true; isPointer: true } + Property { name: "header"; type: "QQuickItem"; isPointer: true } + Property { name: "headerOrientation"; type: "int" } + Property { name: "footer"; type: "QQuickItem"; isPointer: true } + Property { name: "showClickFeedback"; type: "bool" } + } + Component { + prototype: "QQuickGridView" + name: "org.kde.kirigami/CardsGridView 2.4" + exports: ["org.kde.kirigami/CardsGridView 2.4"] + exportMetaObjectRevisions: [4] + isComposite: true + defaultProperty: "delegate" + Property { name: "columns"; type: "int"; isReadonly: true } + Property { name: "maximumColumns"; type: "int" } + Property { name: "maximumColumnWidth"; type: "int" } + Property { name: "minimumColumnWidth"; type: "int" } + Property { name: "delegate"; type: "QQmlComponent"; isPointer: true } + Property { name: "_delegateComponent"; type: "QQmlComponent"; isPointer: true } + } + Component { + prototype: "QQuickGridLayout" + name: "org.kde.kirigami/CardsLayout 2.4" + exports: ["org.kde.kirigami/CardsLayout 2.4"] + exportMetaObjectRevisions: [4] + isComposite: true + defaultProperty: "data" + Property { name: "maximumColumns"; type: "int" } + Property { name: "maximumColumnWidth"; type: "int" } + Property { name: "minimumColumnWidth"; type: "int" } + } + Component { + prototype: "QQuickListView" + name: "org.kde.kirigami/CardsListView 2.4" + exports: ["org.kde.kirigami/CardsListView 2.4"] + exportMetaObjectRevisions: [4] + isComposite: true + defaultProperty: "data" + Property { name: "delegate"; type: "QQmlComponent"; isPointer: true } + Property { name: "_delegateComponent"; type: "QQmlComponent"; isPointer: true } + } + Component { + prototype: "QQuickItemDelegate" + name: "org.kde.kirigami/CheckableListItem 2.14" + exports: ["org.kde.kirigami/CheckableListItem 2.14"] + exportMetaObjectRevisions: [14] + isComposite: true + defaultProperty: "_basicDefault" + Property { name: "leading"; type: "QQuickItem"; isPointer: true } + Property { name: "leadingPadding"; type: "double" } + Property { name: "trailing"; type: "QQuickItem"; isPointer: true } + Property { name: "trailingPadding"; type: "double" } + Property { name: "bold"; type: "bool" } + Property { name: "icon"; type: "QVariant" } + Property { name: "reserveSpaceForSubtitle"; type: "bool" } + Property { name: "fadeContent"; type: "bool" } + Property { name: "label"; type: "string" } + Property { name: "subtitle"; type: "string" } + Property { name: "iconSize"; type: "int" } + Property { name: "iconColor"; type: "QColor" } + Property { name: "reserveSpaceForIcon"; type: "bool" } + Property { name: "reserveSpaceForLabel"; type: "bool" } + Property { name: "textSpacing"; type: "double" } + Property { name: "_basicDefault"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "supportsMouseEvents"; type: "bool" } + Property { name: "alternatingBackground"; type: "bool" } + Property { name: "sectionDelegate"; type: "bool" } + Property { name: "separatorVisible"; type: "bool" } + Property { name: "textColor"; type: "QColor" } + Property { name: "backgroundColor"; type: "QColor" } + Property { name: "alternateBackgroundColor"; type: "QColor" } + Property { name: "activeTextColor"; type: "QColor" } + Property { name: "activeBackgroundColor"; type: "QColor" } + Property { name: "action"; type: "QQuickAction"; isPointer: true } + Property { name: "containsMouse"; type: "bool"; isReadonly: true } + Property { name: "_default"; type: "QQuickItem"; isPointer: true } + } + Component { + prototype: "QQuickDrawer" + name: "org.kde.kirigami/ContextDrawer 2.0" + exports: ["org.kde.kirigami/ContextDrawer 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "contentData" + Property { name: "title"; type: "string" } + Property { name: "actions"; type: "QVariant" } + Property { name: "page"; type: "Page_QMLTYPE_32"; isPointer: true } + Property { name: "header"; type: "QQmlComponent"; isPointer: true } + Property { name: "footer"; type: "QQmlComponent"; isPointer: true } + Property { name: "drawerOpen"; type: "bool" } + Property { name: "enabled"; type: "bool" } + Property { name: "peeking"; type: "bool" } + Property { name: "animating"; type: "bool"; isReadonly: true } + Property { name: "collapsible"; type: "bool" } + Property { name: "collapsed"; type: "bool" } + Property { name: "collapsedSize"; type: "int" } + Property { + name: "handleOpenIcon" + type: "IconPropertiesGroup_QMLTYPE_94" + isReadonly: true + isPointer: true + } + Property { name: "handleClosedIcon"; type: "IconPropertiesGroup_QMLTYPE_94"; isPointer: true } + Property { name: "handleOpenToolTip"; type: "string" } + Property { name: "handleClosedToolTip"; type: "string" } + Property { name: "handleVisible"; type: "bool" } + Property { name: "handle"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "__internal"; type: "QObject"; isPointer: true } + } + Component { + prototype: "QQuickColumnLayout" + name: "org.kde.kirigami/FlexColumn 2.14" + exports: ["org.kde.kirigami/FlexColumn 2.14"] + exportMetaObjectRevisions: [14] + isComposite: true + defaultProperty: "columnChildren" + Enum { + name: "CrossAxis" + values: { + "Left": 0, + "Center": 1, + "Right": 2 + } + } + Property { name: "padding"; type: "double" } + Property { name: "maximumWidth"; type: "double" } + Property { name: "alignment"; type: "int" } + Property { name: "columnChildren"; type: "QQuickItem"; isList: true; isReadonly: true } + } + Component { + prototype: "QQuickItem" + name: "org.kde.kirigami/FormLayout 2.3" + exports: ["org.kde.kirigami/FormLayout 2.3"] + exportMetaObjectRevisions: [3] + isComposite: true + defaultProperty: "data" + Property { name: "wideMode"; type: "bool" } + Property { name: "twinFormLayouts"; type: "QQuickItem"; isList: true; isReadonly: true } + } + Component { + prototype: "QQuickDrawer" + name: "org.kde.kirigami/GlobalDrawer 2.0" + exports: ["org.kde.kirigami/GlobalDrawer 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "content" + Property { name: "actions"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "header"; type: "QQuickItem"; isPointer: true } + Property { name: "bannerVisible"; type: "bool" } + Property { name: "showContentWhenCollapsed"; type: "bool" } + Property { name: "showTopContentWhenCollapsed"; type: "bool" } + Property { name: "showHeaderWhenCollapsed"; type: "bool" } + Property { name: "resetMenuOnTriggered"; type: "bool" } + Property { name: "currentSubMenu"; type: "Action_QMLTYPE_13"; isReadonly: true; isPointer: true } + Property { name: "isMenu"; type: "bool" } + Property { name: "collapseButtonVisible"; type: "bool" } + Property { name: "title"; type: "string" } + Property { name: "titleIcon"; type: "QVariant" } + Property { name: "bannerImageSource"; type: "QUrl" } + Property { name: "content"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "topContent"; type: "QObject"; isList: true; isReadonly: true } + Signal { name: "bannerClicked" } + Method { name: "resetMenu"; type: "QVariant" } + Property { name: "drawerOpen"; type: "bool" } + Property { name: "enabled"; type: "bool" } + Property { name: "peeking"; type: "bool" } + Property { name: "animating"; type: "bool"; isReadonly: true } + Property { name: "collapsible"; type: "bool" } + Property { name: "collapsed"; type: "bool" } + Property { name: "collapsedSize"; type: "int" } + Property { + name: "handleOpenIcon" + type: "IconPropertiesGroup_QMLTYPE_94" + isReadonly: true + isPointer: true + } + Property { name: "handleClosedIcon"; type: "IconPropertiesGroup_QMLTYPE_94"; isPointer: true } + Property { name: "handleOpenToolTip"; type: "string" } + Property { name: "handleClosedToolTip"; type: "string" } + Property { name: "handleVisible"; type: "bool" } + Property { name: "handle"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "__internal"; type: "QObject"; isPointer: true } + } + Component { + prototype: "QQuickLabel" + name: "org.kde.kirigami/Heading 2.0" + exports: ["org.kde.kirigami/Heading 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "data" + Enum { + name: "Type" + values: { + "Normal": 0, + "Primary": 1, + "Secondary": 2 + } + } + Property { name: "level"; type: "int" } + Property { name: "step"; type: "int" } + Property { name: "type"; type: "int" } + Method { + name: "headerPointSize" + type: "QVariant" + Parameter { name: "l"; type: "QVariant" } + } + } + Component { + prototype: "QQuickItem" + name: "org.kde.kirigami/Hero 2.15" + exports: ["org.kde.kirigami/Hero 2.15"] + exportMetaObjectRevisions: [15] + isComposite: true + defaultProperty: "data" + Property { name: "source"; type: "QQuickItem"; isPointer: true } + Property { name: "destination"; type: "QQuickItem"; isPointer: true } + Property { name: "restore"; type: "bool" } + Property { name: "mask"; type: "QObject"; isReadonly: true; isPointer: true } + Property { name: "easing"; type: "QObject"; isReadonly: true; isPointer: true } + Property { name: "duration"; type: "int" } + Method { name: "open"; type: "QVariant" } + Method { name: "close"; type: "QVariant" } + } + Component { + prototype: "QQuickControl" + name: "org.kde.kirigami/InlineMessage 2.4" + exports: ["org.kde.kirigami/InlineMessage 2.4"] + exportMetaObjectRevisions: [4] + isComposite: true + defaultProperty: "data" + Property { name: "type"; type: "int" } + Property { name: "icon"; type: "IconPropertiesGroup_QMLTYPE_94"; isPointer: true } + Property { name: "text"; type: "string" } + Property { name: "showCloseButton"; type: "bool" } + Property { name: "actions"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "animating"; type: "bool"; isReadonly: true } + Property { name: "_animating"; type: "bool" } + Signal { + name: "linkHovered" + Parameter { name: "link"; type: "string" } + } + Signal { + name: "linkActivated" + Parameter { name: "link"; type: "string" } + } + } + Component { + prototype: "QQuickControl" + name: "org.kde.kirigami/ItemViewHeader 2.1" + exports: ["org.kde.kirigami/ItemViewHeader 2.1"] + exportMetaObjectRevisions: [1] + isComposite: true + defaultProperty: "data" + Property { name: "title"; type: "string" } + Property { name: "color"; type: "QColor" } + Property { name: "backgroundImage"; type: "QQuickImage"; isReadonly: true; isPointer: true } + Property { name: "minimumHeight"; type: "int" } + Property { name: "maximumHeight"; type: "int" } + Property { name: "view"; type: "QQuickListView"; isPointer: true } + } + Component { + prototype: "QQuickLabel" + name: "org.kde.kirigami/Label 2.0" + exports: ["org.kde.kirigami/Label 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "data" + } + Component { + prototype: "QQuickLabel" + name: "org.kde.kirigami/LinkButton 2.6" + exports: ["org.kde.kirigami/LinkButton 2.6"] + exportMetaObjectRevisions: [6] + isComposite: true + defaultProperty: "data" + Property { name: "action"; type: "Action_QMLTYPE_13"; isPointer: true } + Property { name: "acceptedButtons"; type: "int" } + Property { name: "mouseArea"; type: "QQuickMouseArea"; isReadonly: true; isPointer: true } + Signal { + name: "pressed" + Parameter { name: "mouse"; type: "QObject"; isPointer: true } + } + Signal { + name: "clicked" + Parameter { name: "mouse"; type: "QObject"; isPointer: true } + } + } + Component { + prototype: "QQuickItem" + name: "org.kde.kirigami/ListItemDragHandle 2.5" + exports: ["org.kde.kirigami/ListItemDragHandle 2.5"] + exportMetaObjectRevisions: [5] + isComposite: true + defaultProperty: "data" + Property { name: "listItem"; type: "QQuickItem"; isPointer: true } + Property { name: "listView"; type: "QQuickListView"; isPointer: true } + Signal { + name: "moveRequested" + Parameter { name: "oldIndex"; type: "int" } + Parameter { name: "newIndex"; type: "int" } + } + Signal { name: "dropped" } + } + Component { + prototype: "QQuickItemDelegate" + name: "org.kde.kirigami/ListSectionHeader 2.10" + exports: ["org.kde.kirigami/ListSectionHeader 2.10"] + exportMetaObjectRevisions: [10] + isComposite: true + defaultProperty: "_contents" + Property { name: "label"; type: "string" } + Property { name: "_contents"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "supportsMouseEvents"; type: "bool" } + Property { name: "alternatingBackground"; type: "bool" } + Property { name: "sectionDelegate"; type: "bool" } + Property { name: "separatorVisible"; type: "bool" } + Property { name: "textColor"; type: "QColor" } + Property { name: "backgroundColor"; type: "QColor" } + Property { name: "alternateBackgroundColor"; type: "QColor" } + Property { name: "activeTextColor"; type: "QColor" } + Property { name: "activeBackgroundColor"; type: "QColor" } + Property { name: "action"; type: "QQuickAction"; isPointer: true } + Property { name: "containsMouse"; type: "bool"; isReadonly: true } + Property { name: "_default"; type: "QQuickItem"; isPointer: true } + } + Component { + prototype: "QQuickDrawer" + name: "org.kde.kirigami/OverlayDrawer 2.0" + exports: ["org.kde.kirigami/OverlayDrawer 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "contentData" + Property { name: "drawerOpen"; type: "bool" } + Property { name: "enabled"; type: "bool" } + Property { name: "peeking"; type: "bool" } + Property { name: "animating"; type: "bool"; isReadonly: true } + Property { name: "collapsible"; type: "bool" } + Property { name: "collapsed"; type: "bool" } + Property { name: "collapsedSize"; type: "int" } + Property { + name: "handleOpenIcon" + type: "IconPropertiesGroup_QMLTYPE_94" + isReadonly: true + isPointer: true + } + Property { name: "handleClosedIcon"; type: "IconPropertiesGroup_QMLTYPE_94"; isPointer: true } + Property { name: "handleOpenToolTip"; type: "string" } + Property { name: "handleClosedToolTip"; type: "string" } + Property { name: "handleVisible"; type: "bool" } + Property { name: "handle"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "__internal"; type: "QObject"; isPointer: true } + } + Component { + prototype: "QObject" + name: "org.kde.kirigami/OverlaySheet 2.0" + exports: ["org.kde.kirigami/OverlaySheet 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "contentItem" + Property { name: "contentItem"; type: "QQuickItem"; isPointer: true } + Property { name: "sheetOpen"; type: "bool" } + Property { name: "leftPadding"; type: "int" } + Property { name: "topPadding"; type: "int" } + Property { name: "rightPadding"; type: "int" } + Property { name: "bottomPadding"; type: "int" } + Property { name: "leftInset"; type: "double" } + Property { name: "topInset"; type: "double" } + Property { name: "rightInset"; type: "double" } + Property { name: "bottomInset"; type: "double" } + Property { name: "header"; type: "QQuickItem"; isPointer: true } + Property { name: "footer"; type: "QQuickItem"; isPointer: true } + Property { name: "background"; type: "QQuickItem"; isPointer: true } + Property { name: "showCloseButton"; type: "bool" } + Property { name: "parent"; type: "QQuickItem"; isPointer: true } + Property { name: "rootItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Method { name: "open"; type: "QVariant" } + Method { name: "close"; type: "QVariant" } + } + Component { + prototype: "QQuickPage" + name: "org.kde.kirigami/Page 2.0" + exports: ["org.kde.kirigami/Page 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "contentData" + Property { name: "flickable"; type: "QQuickFlickable"; isPointer: true } + Property { name: "isCurrentPage"; type: "bool"; isReadonly: true } + Property { name: "icon"; type: "ActionIconGroup_QMLTYPE_29"; isPointer: true } + Property { name: "needsAttention"; type: "bool" } + Property { name: "progress"; type: "QVariant" } + Property { name: "titleDelegate"; type: "QQmlComponent"; isPointer: true } + Property { name: "globalToolBarItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "globalToolBarStyle"; type: "int" } + Property { name: "contextualActions"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "mainAction"; type: "QObject"; isPointer: true } + Property { name: "leftAction"; type: "QObject"; isPointer: true } + Property { name: "rightAction"; type: "QObject"; isPointer: true } + Property { + name: "actions" + type: "PageActionPropertyGroup_QMLTYPE_30" + isReadonly: true + isPointer: true + } + Property { name: "overlay"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Signal { name: "contextualActionsAboutToShow" } + Signal { + name: "backRequested" + Parameter { name: "event"; type: "QVariant" } + } + } + Component { + prototype: "QQuickAction" + name: "org.kde.kirigami/PagePoolAction 2.11" + exports: ["org.kde.kirigami/PagePoolAction 2.11"] + exportMetaObjectRevisions: [11] + isComposite: true + defaultProperty: "children" + Property { name: "page"; type: "string" } + Property { name: "pagePool"; type: "PagePool"; isPointer: true } + Property { name: "pageStack"; type: "QQuickItem"; isPointer: true } + Property { name: "basePage"; type: "Page_QMLTYPE_31"; isPointer: true } + Property { name: "initialProperties"; type: "QVariant" } + Property { name: "useLayers"; type: "bool" } + Property { name: "_private"; type: "QObject"; isPointer: true } + Method { name: "pageItem"; type: "QVariant" } + Method { name: "layerContainsPage"; type: "QVariant" } + Method { name: "stackContainsPage"; type: "QVariant" } + Enum { + name: "DisplayHint" + values: { + "NoPreference": 0, + "IconOnly": 1, + "KeepVisible": 2, + "AlwaysHide": 4, + "HideChildIndicator": 8 + } + } + Property { name: "visible"; type: "bool" } + Property { name: "tooltip"; type: "string" } + Property { name: "separator"; type: "bool" } + Property { name: "expandible"; type: "bool" } + Property { name: "parent"; type: "Action_QMLTYPE_12"; isPointer: true } + Property { name: "displayHint"; type: "int" } + Property { name: "displayComponent"; type: "QQmlComponent"; isPointer: true } + Property { name: "__children"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "visibleChildren"; type: "QVariant"; isReadonly: true } + Property { name: "iconName"; type: "string" } + Property { name: "iconSource"; type: "QUrl" } + Property { name: "children"; type: "QObject"; isList: true; isReadonly: true } + Method { + name: "displayHintSet" + type: "QVariant" + Parameter { name: "hint"; type: "QVariant" } + } + } + Component { + prototype: "QQuickControl" + name: "org.kde.kirigami/PageRow 2.0" + exports: ["org.kde.kirigami/PageRow 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "data" + Property { name: "lastItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "initialPage"; type: "QVariant" } + Property { name: "defaultColumnWidth"; type: "int" } + Property { name: "wideMode"; type: "bool"; isReadonly: true } + Property { name: "depth"; type: "int"; isReadonly: true } + Property { name: "currentItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "currentIndex"; type: "int" } + Property { name: "columnView"; type: "ColumnView"; isReadonly: true; isPointer: true } + Property { name: "items"; type: "QQuickItem"; isList: true; isReadonly: true } + Property { name: "visibleItems"; type: "QList"; isReadonly: true } + Property { name: "firstVisibleItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "lastVisibleItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "interactive"; type: "bool" } + Property { name: "separatorVisible"; type: "bool" } + Property { + name: "globalToolBar" + type: "PageRowGlobalToolBarStyleGroup_QMLTYPE_21" + isReadonly: true + isPointer: true + } + Property { name: "layers"; type: "StackView_QMLTYPE_22"; isReadonly: true; isPointer: true } + Signal { + name: "pageInserted" + Parameter { name: "position"; type: "int" } + Parameter { name: "page"; type: "QQuickItem"; isPointer: true } + } + Signal { + name: "pagePushed" + Parameter { name: "page"; type: "QQuickItem"; isPointer: true } + } + Signal { + name: "pageRemoved" + Parameter { name: "page"; type: "QQuickItem"; isPointer: true } + } + Method { + name: "push" + type: "QVariant" + Parameter { name: "page"; type: "QVariant" } + Parameter { name: "properties"; type: "QVariant" } + } + Method { + name: "insertPage" + type: "QVariant" + Parameter { name: "position"; type: "QVariant" } + Parameter { name: "page"; type: "QVariant" } + Parameter { name: "properties"; type: "QVariant" } + } + Method { + name: "movePage" + type: "QVariant" + Parameter { name: "fromPos"; type: "QVariant" } + Parameter { name: "toPos"; type: "QVariant" } + } + Method { + name: "removePage" + type: "QVariant" + Parameter { name: "page"; type: "QVariant" } + } + Method { + name: "pop" + type: "QVariant" + Parameter { name: "page"; type: "QVariant" } + } + Method { + name: "replace" + type: "QVariant" + Parameter { name: "page"; type: "QVariant" } + Parameter { name: "properties"; type: "QVariant" } + } + Method { name: "clear"; type: "QVariant" } + Method { + name: "get" + type: "QVariant" + Parameter { name: "idx"; type: "QVariant" } + } + Method { name: "flickBack"; type: "QVariant" } + } + Component { + prototype: "QQuickTextField" + name: "org.kde.kirigami/PasswordField 2.8" + exports: ["org.kde.kirigami/PasswordField 2.8"] + exportMetaObjectRevisions: [8] + isComposite: true + defaultProperty: "data" + Property { name: "showPassword"; type: "bool" } + Property { name: "focusSequence"; type: "string" } + Property { name: "leftActions"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "rightActions"; type: "QObject"; isList: true; isReadonly: true } + } + Component { + prototype: "QQuickColumnLayout" + name: "org.kde.kirigami/PlaceholderMessage 2.12" + exports: ["org.kde.kirigami/PlaceholderMessage 2.12"] + exportMetaObjectRevisions: [12] + isComposite: true + defaultProperty: "data" + Property { name: "text"; type: "string" } + Property { name: "explanation"; type: "string" } + Property { name: "icon"; type: "ActionIconGroup_QMLTYPE_29"; isPointer: true } + Property { name: "helpfulAction"; type: "QQuickAction"; isPointer: true } + } + Component { + prototype: "QQuickApplicationWindow" + name: "org.kde.kirigami/RouterWindow 2.12" + exports: ["org.kde.kirigami/RouterWindow 2.12"] + exportMetaObjectRevisions: [12] + isComposite: true + defaultProperty: "routes" + Property { name: "routes"; type: "PageRoute"; isList: true; isReadonly: true } + Property { name: "initialRoute"; type: "QJSValue" } + Property { name: "router"; type: "PageRouter"; isReadonly: true; isPointer: true } + Property { name: "pageStack"; type: "PageRow_QMLTYPE_23"; isReadonly: true; isPointer: true } + Property { name: "controlsVisible"; type: "bool" } + Property { name: "globalDrawer"; type: "OverlayDrawer_QMLTYPE_113"; isPointer: true } + Property { name: "wideScreen"; type: "bool" } + Property { name: "contextDrawer"; type: "OverlayDrawer_QMLTYPE_113"; isPointer: true } + Property { name: "reachableMode"; type: "bool" } + Property { name: "reachableModeEnabled"; type: "bool" } + Property { name: "quitAction"; type: "Action_QMLTYPE_13"; isReadonly: true; isPointer: true } + Method { + name: "showPassiveNotification" + type: "QVariant" + Parameter { name: "message"; type: "QVariant" } + Parameter { name: "timeout"; type: "QVariant" } + Parameter { name: "actionText"; type: "QVariant" } + Parameter { name: "callBack"; type: "QVariant" } + } + Method { name: "hidePassiveNotification"; type: "QVariant" } + Method { name: "applicationWindow"; type: "QVariant" } + } + Component { + prototype: "QQuickPage" + name: "org.kde.kirigami/ScrollablePage 2.0" + exports: ["org.kde.kirigami/ScrollablePage 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "mainItem" + Property { name: "mainItem"; type: "QObject"; isPointer: true } + Property { name: "keyboardNavigationEnabled"; type: "bool" } + Property { name: "refreshing"; type: "bool" } + Property { name: "supportsRefreshing"; type: "bool" } + Property { name: "flickable"; type: "QQuickFlickable"; isPointer: true } + Property { name: "verticalScrollBarPolicy"; type: "int" } + Property { name: "horizontalScrollBarPolicy"; type: "int" } + Property { name: "isCurrentPage"; type: "bool"; isReadonly: true } + Property { name: "icon"; type: "ActionIconGroup_QMLTYPE_29"; isPointer: true } + Property { name: "needsAttention"; type: "bool" } + Property { name: "progress"; type: "QVariant" } + Property { name: "titleDelegate"; type: "QQmlComponent"; isPointer: true } + Property { name: "globalToolBarItem"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Property { name: "globalToolBarStyle"; type: "int" } + Property { name: "contextualActions"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "mainAction"; type: "QObject"; isPointer: true } + Property { name: "leftAction"; type: "QObject"; isPointer: true } + Property { name: "rightAction"; type: "QObject"; isPointer: true } + Property { + name: "actions" + type: "PageActionPropertyGroup_QMLTYPE_30" + isReadonly: true + isPointer: true + } + Property { name: "overlay"; type: "QQuickItem"; isReadonly: true; isPointer: true } + Signal { name: "contextualActionsAboutToShow" } + Signal { + name: "backRequested" + Parameter { name: "event"; type: "QVariant" } + } + } + Component { + prototype: "QQuickTextField" + name: "org.kde.kirigami/SearchField 2.8" + exports: ["org.kde.kirigami/SearchField 2.8"] + exportMetaObjectRevisions: [8] + isComposite: true + defaultProperty: "data" + Property { name: "autoAccept"; type: "bool" } + Property { name: "delaySearch"; type: "bool" } + Property { name: "focusSequence"; type: "string" } + Property { name: "leftActions"; type: "QObject"; isList: true; isReadonly: true } + Property { name: "rightActions"; type: "QObject"; isList: true; isReadonly: true } + } + Component { + prototype: "QQuickTextArea" + name: "org.kde.kirigami/SelectableLabel 2.20" + exports: ["org.kde.kirigami/SelectableLabel 2.20"] + exportMetaObjectRevisions: [20] + isComposite: true + defaultProperty: "data" + Property { name: "cursorShape"; type: "Qt::CursorShape" } + } + Component { + prototype: "QQuickRectangle" + name: "org.kde.kirigami/Separator 2.0" + exports: ["org.kde.kirigami/Separator 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "data" + Enum { + name: "Weight" + values: { + "Light": 0, + "Normal": 1 + } + } + Property { name: "weight"; type: "int" } + } + Component { + prototype: "QQuickItem" + name: "org.kde.kirigami/ShadowedImage 2.12" + exports: ["org.kde.kirigami/ShadowedImage 2.12"] + exportMetaObjectRevisions: [12] + isComposite: true + defaultProperty: "data" + Property { name: "color"; type: "QColor" } + Property { name: "radius"; type: "double" } + Property { name: "shadow"; type: "ShadowGroup"; isReadonly: true; isPointer: true } + Property { name: "border"; type: "BorderGroup"; isReadonly: true; isPointer: true } + Property { name: "corners"; type: "CornersGroup"; isReadonly: true; isPointer: true } + Property { name: "source"; type: "QUrl" } + Property { name: "asynchronous"; type: "bool" } + Property { name: "fillMode"; type: "int" } + Property { name: "sourceSize"; type: "QSize" } + } + Component { + prototype: "QQuickSwipeDelegate" + name: "org.kde.kirigami/SwipeListItem 2.0" + exports: ["org.kde.kirigami/SwipeListItem 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + defaultProperty: "_default" + Property { name: "alternatingBackground"; type: "bool" } + Property { name: "sectionDelegate"; type: "bool" } + Property { name: "separatorVisible"; type: "bool" } + Property { name: "actionsVisible"; type: "bool"; isReadonly: true } + Property { name: "actions"; type: "Action_QMLTYPE_12"; isList: true; isReadonly: true } + Property { name: "textColor"; type: "QColor" } + Property { name: "backgroundColor"; type: "QColor" } + Property { name: "alternateBackgroundColor"; type: "QColor" } + Property { name: "activeTextColor"; type: "QColor" } + Property { name: "activeBackgroundColor"; type: "QColor" } + Property { name: "alwaysVisibleActions"; type: "bool" } + Property { name: "supportsMouseEvents"; type: "bool" } + Property { name: "containsMouse"; type: "bool"; isReadonly: true } + Property { name: "_default"; type: "QQuickItem"; isPointer: true } + } + Component { + prototype: "QQuickItem" + name: "org.kde.kirigami/SwipeNavigator 2.13" + exports: ["org.kde.kirigami/SwipeNavigator 2.13"] + exportMetaObjectRevisions: [13] + isComposite: true + defaultProperty: "pages" + Property { name: "pages"; type: "Page_QMLTYPE_32"; isList: true; isReadonly: true } + Property { name: "big"; type: "bool" } + Property { name: "header"; type: "QQmlComponent"; isPointer: true } + Property { name: "footer"; type: "QQmlComponent"; isPointer: true } + Property { name: "initialIndex"; type: "int" } + Property { name: "layers"; type: "StackView_QMLTYPE_22"; isReadonly: true; isPointer: true } + Property { name: "currentIndex"; type: "int" } + } + Component { + prototype: "QObject" + name: "org.kde.kirigami/Units 2.0" + exports: ["org.kde.kirigami/Units 2.0"] + exportMetaObjectRevisions: [0] + isComposite: true + isCreatable: false + isSingleton: true + Property { name: "gridUnit"; type: "int" } + Property { name: "iconSizes"; type: "QObject"; isPointer: true } + Property { name: "smallSpacing"; type: "int" } + Property { name: "largeSpacing"; type: "int" } + Property { name: "devicePixelRatio"; type: "double" } + Property { name: "veryLongDuration"; type: "int" } + Property { name: "longDuration"; type: "int" } + Property { name: "shortDuration"; type: "int" } + Property { name: "veryShortDuration"; type: "int" } + Property { name: "toolTipDelay"; type: "int" } + Property { name: "humanMoment"; type: "int" } + Property { name: "wheelScrollLines"; type: "int"; isReadonly: true } + Property { name: "fontMetrics"; type: "QVariant" } + } + Component { + prototype: "QQuickLabel" + name: "org.kde.kirigami/UrlButton 2.6" + exports: ["org.kde.kirigami/UrlButton 2.6"] + exportMetaObjectRevisions: [6] + isComposite: true + defaultProperty: "data" + Property { name: "url"; type: "string" } + Property { name: "action"; type: "Action_QMLTYPE_13"; isPointer: true } + Property { name: "acceptedButtons"; type: "int" } + Property { name: "mouseArea"; type: "QQuickMouseArea"; isReadonly: true; isPointer: true } + Signal { + name: "pressed" + Parameter { name: "mouse"; type: "QObject"; isPointer: true } + } + Signal { + name: "clicked" + Parameter { name: "mouse"; type: "QObject"; isPointer: true } + } + } +} diff --git a/src/scenegraph/managedtexturenode.cpp b/src/scenegraph/managedtexturenode.cpp new file mode 100644 index 0000000..5c021b1 --- /dev/null +++ b/src/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/scenegraph/managedtexturenode.h b/src/scenegraph/managedtexturenode.h new file mode 100644 index 0000000..b65a498 --- /dev/null +++ b/src/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/scenegraph/paintedrectangleitem.cpp b/src/scenegraph/paintedrectangleitem.cpp new file mode 100644 index 0000000..63e6df6 --- /dev/null +++ b/src/scenegraph/paintedrectangleitem.cpp @@ -0,0 +1,56 @@ +/* + * 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); +} diff --git a/src/scenegraph/paintedrectangleitem.h b/src/scenegraph/paintedrectangleitem.h new file mode 100644 index 0000000..8036682 --- /dev/null +++ b/src/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/scenegraph/shaders/header_desktop.glsl b/src/scenegraph/shaders/header_desktop.glsl new file mode 100644 index 0000000..53b0dd5 --- /dev/null +++ b/src/scenegraph/shaders/header_desktop.glsl @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// This file contains common directives needed for the shaders to work. +// It is included as the very first bit in the shader. +// Important: If a specific GLSL version is needed, it should be set in this +// file. + +// This file is intended for desktop OpenGL version 2.1 or greater. + +#version 120 + +#ifndef lowp + #define lowp +#endif + +#ifndef mediump + #define mediump +#endif + +#ifndef highp + #define highp mediump +#endif diff --git a/src/scenegraph/shaders/header_desktop_core.glsl b/src/scenegraph/shaders/header_desktop_core.glsl new file mode 100644 index 0000000..fbec34d --- /dev/null +++ b/src/scenegraph/shaders/header_desktop_core.glsl @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// This file contains common directives needed for the shaders to work. +// It is included as the very first bit in the shader. +// Important: If a specific GLSL version is needed, it should be set in this +// file. + +// This file is intended for desktop OpenGL version 4.5 or greater. + +#version 450 + +#define CORE_PROFILE diff --git a/src/scenegraph/shaders/header_es.glsl b/src/scenegraph/shaders/header_es.glsl new file mode 100644 index 0000000..6a58d4c --- /dev/null +++ b/src/scenegraph/shaders/header_es.glsl @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// This file contains common directives needed for the shaders to work. +// It is included as the very first bit in the shader. +// Important: If a specific GLSL version is needed, it should be set in this +// file. + +// This file is intended for OpenGLES version 2.0 or greater. + +#version 100 +#extension GL_OES_standard_derivatives : enable + diff --git a/src/scenegraph/shaders/sdf.glsl b/src/scenegraph/shaders/sdf.glsl new file mode 100644 index 0000000..69402c5 --- /dev/null +++ b/src/scenegraph/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/scenegraph/shaders/sdf_lowpower.glsl b/src/scenegraph/shaders/sdf_lowpower.glsl new file mode 100644 index 0000000..8cdc364 --- /dev/null +++ b/src/scenegraph/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/scenegraph/shaders/shaders.qrc b/src/scenegraph/shaders/shaders.qrc new file mode 100644 index 0000000..d2ad90f --- /dev/null +++ b/src/scenegraph/shaders/shaders.qrc @@ -0,0 +1,26 @@ + + + + header_es.glsl + header_desktop.glsl + header_desktop_core.glsl + sdf.glsl + sdf_lowpower.glsl + sdf.glsl + shadowedrectangle.vert + shadowedrectangle.vert + shadowedrectangle.frag + shadowedrectangle_lowpower.frag + shadowedrectangle.frag + shadowedborderrectangle.frag + shadowedborderrectangle_lowpower.frag + shadowedborderrectangle.frag + shadowedtexture.frag + shadowedtexture_lowpower.frag + shadowedtexture.frag + shadowedbordertexture.frag + shadowedbordertexture_lowpower.frag + shadowedbordertexture.frag + + + diff --git a/src/scenegraph/shaders/shadowedborderrectangle.frag b/src/scenegraph/shaders/shadowedborderrectangle.frag new file mode 100644 index 0000000..5a68375 --- /dev/null +++ b/src/scenegraph/shaders/shadowedborderrectangle.frag @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// 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. + +uniform lowp float opacity; +uniform lowp float size; +uniform lowp vec4 radius; +uniform lowp vec4 color; +uniform lowp vec4 shadowColor; +uniform lowp vec2 offset; +uniform lowp vec2 aspect; +uniform lowp float borderWidth; +uniform lowp vec4 borderColor; + +#ifdef CORE_PROFILE +in lowp vec2 uv; +out lowp vec4 out_color; +#else +varying lowp vec2 uv; +#define out_color gl_FragColor +#endif + +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 + size + length(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(radius, minimum_shadow_radius)); + lowp vec4 shadow_radius = radius + size * size_factor; + + lowp vec4 col = vec4(0.0); + + // Calculate the shadow's distance field. + lowp float shadow = sdf_rounded_rectangle(uv - offset * 2.0 * inverse_scale, aspect * inverse_scale, shadow_radius * inverse_scale); + // Render it, interpolating the color over the distance. + col = mix(col, shadowColor * sign(size), 1.0 - smoothstep(-size * 0.5, size * 0.5, shadow)); + + // Scale corrected corner radius + lowp vec4 corner_radius = radius * inverse_scale; + + // Calculate the outer rectangle distance field and render it. + lowp float outer_rect = sdf_rounded_rectangle(uv, aspect * inverse_scale, corner_radius); + + col = sdf_render(outer_rect, col, borderColor); + + // The inner rectangle distance field is the outer reduced by twice the border size. + lowp float inner_rect = outer_rect + (borderWidth * inverse_scale) * 2.0; + + // Finally, render the inner rectangle. + col = sdf_render(inner_rect, col, color); + + out_color = col * opacity; +} diff --git a/src/scenegraph/shaders/shadowedborderrectangle_lowpower.frag b/src/scenegraph/shaders/shadowedborderrectangle_lowpower.frag new file mode 100644 index 0000000..5868e97 --- /dev/null +++ b/src/scenegraph/shaders/shadowedborderrectangle_lowpower.frag @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// 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. + +uniform lowp float opacity; +uniform lowp float size; +uniform lowp vec4 radius; +uniform lowp vec4 color; +uniform lowp vec4 shadowColor; +uniform lowp vec2 offset; +uniform lowp vec2 aspect; +uniform lowp float borderWidth; +uniform lowp vec4 borderColor; + +#ifdef CORE_PROFILE +in lowp vec2 uv; +out lowp vec4 out_color; +#else +varying lowp vec2 uv; +#define out_color gl_FragColor +#define texture texture2D +#endif + +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, aspect, radius); + + col = sdf_render(outer_rect, col, borderColor); + + // The inner distance field is the outer reduced by border width. + lowp float inner_rect = outer_rect + borderWidth * 2.0; + + // Render it. + col = sdf_render(inner_rect, col, color); + + out_color = col * opacity; +} diff --git a/src/scenegraph/shaders/shadowedbordertexture.frag b/src/scenegraph/shaders/shadowedbordertexture.frag new file mode 100644 index 0000000..7ea011d --- /dev/null +++ b/src/scenegraph/shaders/shadowedbordertexture.frag @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// 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. + +uniform lowp float opacity; +uniform lowp float size; +uniform lowp vec4 radius; +uniform lowp vec4 color; +uniform lowp vec4 shadowColor; +uniform lowp vec2 offset; +uniform lowp vec2 aspect; +uniform lowp float borderWidth; +uniform lowp vec4 borderColor; +uniform sampler2D textureSource; + +#ifdef CORE_PROFILE +in lowp vec2 uv; +out lowp vec4 out_color; +#else +varying lowp vec2 uv; +#define out_color gl_FragColor +#define texture texture2D +#endif + +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 + size + length(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(radius, minimum_shadow_radius)); + lowp vec4 shadow_radius = radius + size * size_factor; + + lowp vec4 col = vec4(0.0); + + // Calculate the shadow's distance field. + lowp float shadow = sdf_rounded_rectangle(uv - offset * 2.0 * inverse_scale, aspect * inverse_scale, shadow_radius * inverse_scale); + // Render it, interpolating the color over the distance. + col = mix(col, shadowColor * sign(size), 1.0 - smoothstep(-size * 0.5, size * 0.5, shadow)); + + // Scale corrected corner radius + lowp vec4 corner_radius = radius * inverse_scale; + + // Calculate the outer rectangle distance field and render it. + lowp float outer_rect = sdf_rounded_rectangle(uv, aspect * inverse_scale, corner_radius); + + col = sdf_render(outer_rect, col, borderColor); + + // The inner rectangle distance field is the outer reduced by twice the border width. + lowp float inner_rect = outer_rect + (borderWidth * inverse_scale) * 2.0; + + // Render the inner rectangle. + col = sdf_render(inner_rect, col, color); + + // Sample the texture, then blend it on top of the background color. + lowp vec2 texture_uv = ((uv / 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 * opacity; +} diff --git a/src/scenegraph/shaders/shadowedbordertexture_lowpower.frag b/src/scenegraph/shaders/shadowedbordertexture_lowpower.frag new file mode 100644 index 0000000..a2f5fc9 --- /dev/null +++ b/src/scenegraph/shaders/shadowedbordertexture_lowpower.frag @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// 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. + +uniform lowp float opacity; +uniform lowp float size; +uniform lowp vec4 radius; +uniform lowp vec4 color; +uniform lowp vec4 shadowColor; +uniform lowp vec2 offset; +uniform lowp vec2 aspect; +uniform lowp float borderWidth; +uniform lowp vec4 borderColor; +uniform sampler2D textureSource; + +#ifdef CORE_PROFILE +in lowp vec2 uv; +out lowp vec4 out_color; +#else +varying lowp vec2 uv; +#define out_color gl_FragColor +#define texture texture2D +#endif + +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, aspect, radius); + + // Render it + col = sdf_render(outer_rect, col, borderColor); + + // Inner rectangle distance field equals outer reduced by twice the border width + lowp float inner_rect = outer_rect + borderWidth * 2.0; + + // Render it so we have a background for the image. + col = sdf_render(inner_rect, col, color); + + // Sample the texture, then render it, blending with the background color. + lowp vec2 texture_uv = ((uv / 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 * opacity; +} diff --git a/src/scenegraph/shaders/shadowedrectangle.frag b/src/scenegraph/shaders/shadowedrectangle.frag new file mode 100644 index 0000000..cbbd73d --- /dev/null +++ b/src/scenegraph/shaders/shadowedrectangle.frag @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// See sdf.glsl for the SDF related functions. + +// This shader renders a rectangle with rounded corners and a shadow below it. + +uniform lowp float opacity; +uniform lowp float size; +uniform lowp vec4 radius; +uniform lowp vec4 color; +uniform lowp vec4 shadowColor; +uniform lowp vec2 offset; +uniform lowp vec2 aspect; + +#ifdef CORE_PROFILE +in lowp vec2 uv; +out lowp vec4 out_color; +#else +varying lowp vec2 uv; +#define out_color gl_FragColor +#endif + +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 + size + length(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(radius, minimum_shadow_radius)); + lowp vec4 shadow_radius = radius + size * size_factor; + + lowp vec4 col = vec4(0.0); + + // Calculate the shadow's distance field. + lowp float shadow = sdf_rounded_rectangle(uv - offset * 2.0 * inverse_scale, aspect * inverse_scale, shadow_radius * inverse_scale); + // Render it, interpolating the color over the distance. + col = mix(col, shadowColor * sign(size), 1.0 - smoothstep(-size * 0.5, size * 0.5, shadow)); + + // Calculate the main rectangle distance field and render it. + lowp float rect = sdf_rounded_rectangle(uv, aspect * inverse_scale, radius * inverse_scale); + + col = sdf_render(rect, col, color); + + out_color = col * opacity; +} diff --git a/src/scenegraph/shaders/shadowedrectangle.vert b/src/scenegraph/shaders/shadowedrectangle.vert new file mode 100644 index 0000000..f27244c --- /dev/null +++ b/src/scenegraph/shaders/shadowedrectangle.vert @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +uniform highp mat4 matrix; +uniform lowp vec2 aspect; + +#ifdef CORE_PROFILE +in highp vec4 in_vertex; +in mediump vec2 in_uv; +out mediump vec2 uv; +#else +attribute highp vec4 in_vertex; +attribute mediump vec2 in_uv; +varying mediump vec2 uv; +#endif + +void main() { + uv = (-1.0 + 2.0 * in_uv) * aspect; + gl_Position = matrix * in_vertex; +} diff --git a/src/scenegraph/shaders/shadowedrectangle_lowpower.frag b/src/scenegraph/shaders/shadowedrectangle_lowpower.frag new file mode 100644 index 0000000..92d777d --- /dev/null +++ b/src/scenegraph/shaders/shadowedrectangle_lowpower.frag @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// See sdf.glsl for the SDF related functions. + +// 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. + +uniform lowp float opacity; +uniform lowp float size; +uniform lowp vec4 radius; +uniform lowp vec4 color; +uniform lowp vec4 shadowColor; +uniform lowp vec2 offset; +uniform lowp vec2 aspect; + +#ifdef CORE_PROFILE +in lowp vec2 uv; +out lowp vec4 out_color; +#else +varying lowp vec2 uv; +#define out_color gl_FragColor +#endif + +void main() +{ + lowp vec4 col = vec4(0.0); + + // Calculate the main rectangle distance field. + lowp float rect = sdf_rounded_rectangle(uv, aspect, radius); + + // Render it. + col = sdf_render(rect, col, color); + + out_color = col * opacity; +} diff --git a/src/scenegraph/shaders/shadowedtexture.frag b/src/scenegraph/shaders/shadowedtexture.frag new file mode 100644 index 0000000..0bb0112 --- /dev/null +++ b/src/scenegraph/shaders/shadowedtexture.frag @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// 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. + +uniform lowp float opacity; +uniform lowp float size; +uniform lowp vec4 radius; +uniform lowp vec4 color; +uniform lowp vec4 shadowColor; +uniform lowp vec2 offset; +uniform lowp vec2 aspect; +uniform sampler2D textureSource; + +#ifdef CORE_PROFILE +in lowp vec2 uv; +out lowp vec4 out_color; +#else +varying lowp vec2 uv; +#define out_color gl_FragColor +#define texture texture2D +#endif + +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 + size + length(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(radius, minimum_shadow_radius)); + lowp vec4 shadow_radius = radius + size * size_factor; + + lowp vec4 col = vec4(0.0); + + // Calculate the shadow's distance field. + lowp float shadow = sdf_rounded_rectangle(uv - offset * 2.0 * inverse_scale, aspect * inverse_scale, shadow_radius * inverse_scale); + // Render it, interpolating the color over the distance. + col = mix(col, shadowColor * sign(size), 1.0 - smoothstep(-size * 0.5, size * 0.5, shadow)); + + // Calculate the main rectangle distance field and render it. + lowp float rect = sdf_rounded_rectangle(uv, aspect * inverse_scale, radius * inverse_scale); + + col = sdf_render(rect, col, color); + + // Sample the texture, then blend it on top of the background color. + lowp vec2 texture_uv = ((uv / 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 * opacity; +} diff --git a/src/scenegraph/shaders/shadowedtexture_lowpower.frag b/src/scenegraph/shaders/shadowedtexture_lowpower.frag new file mode 100644 index 0000000..d77c121 --- /dev/null +++ b/src/scenegraph/shaders/shadowedtexture_lowpower.frag @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// 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. + +uniform lowp float opacity; +uniform lowp float size; +uniform lowp vec4 radius; +uniform lowp vec4 color; +uniform lowp vec4 shadowColor; +uniform lowp vec2 offset; +uniform lowp vec2 aspect; +uniform sampler2D textureSource; + +#ifdef CORE_PROFILE +in lowp vec2 uv; +out lowp vec4 out_color; +#else +varying lowp vec2 uv; +#define out_color gl_FragColor +#define texture texture2D +#endif + +void main() +{ + lowp vec4 col = vec4(0.0); + + // Calculate the main rectangle distance field. + lowp float rect = sdf_rounded_rectangle(uv, aspect, radius); + + // Render it, so we have a background for the image. + col = sdf_render(rect, col, color); + + // Sample the texture, then render it, blending it with the background. + lowp vec2 texture_uv = ((uv / 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 * opacity; +} diff --git a/src/scenegraph/shadowedborderrectanglematerial.cpp b/src/scenegraph/shadowedborderrectanglematerial.cpp new file mode 100644 index 0000000..8eaf08b --- /dev/null +++ b/src/scenegraph/shadowedborderrectanglematerial.cpp @@ -0,0 +1,67 @@ +/* + * 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() 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 result; +} + +ShadowedBorderRectangleShader::ShadowedBorderRectangleShader(ShadowedRectangleMaterial::ShaderType shaderType) + : ShadowedRectangleShader(shaderType) +{ + setShader(shaderType, QStringLiteral("shadowedborderrectangle")); +} + +void ShadowedBorderRectangleShader::initialize() +{ + ShadowedRectangleShader::initialize(); + m_borderWidthLocation = program()->uniformLocation("borderWidth"); + m_borderColorLocation = program()->uniformLocation("borderColor"); +} + +void ShadowedBorderRectangleShader::updateState(const QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) +{ + ShadowedRectangleShader::updateState(state, newMaterial, oldMaterial); + + auto p = program(); + + if (!oldMaterial || newMaterial->compare(oldMaterial) != 0 || state.isCachedMaterialDataDirty()) { + auto material = static_cast(newMaterial); + p->setUniformValue(m_borderWidthLocation, material->borderWidth); + p->setUniformValue(m_borderColorLocation, material->borderColor); + } +} diff --git a/src/scenegraph/shadowedborderrectanglematerial.h b/src/scenegraph/shadowedborderrectanglematerial.h new file mode 100644 index 0000000..261b589 --- /dev/null +++ b/src/scenegraph/shadowedborderrectanglematerial.h @@ -0,0 +1,43 @@ +/* + * 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() 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); + + void initialize() override; + void updateState(const QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; + +private: + int m_borderWidthLocation = -1; + int m_borderColorLocation = -1; +}; diff --git a/src/scenegraph/shadowedbordertexturematerial.cpp b/src/scenegraph/shadowedbordertexturematerial.cpp new file mode 100644 index 0000000..18aef93 --- /dev/null +++ b/src/scenegraph/shadowedbordertexturematerial.cpp @@ -0,0 +1,65 @@ +/* + * 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() 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 result; +} + +ShadowedBorderTextureShader::ShadowedBorderTextureShader(ShadowedRectangleMaterial::ShaderType shaderType) + : ShadowedBorderRectangleShader(shaderType) +{ + setShader(shaderType, QStringLiteral("shadowedbordertexture")); +} + +void ShadowedBorderTextureShader::initialize() +{ + ShadowedBorderRectangleShader::initialize(); + program()->setUniformValue("textureSource", 0); +} + +void ShadowedBorderTextureShader::updateState(const QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) +{ + ShadowedBorderRectangleShader::updateState(state, newMaterial, oldMaterial); + + auto texture = static_cast(newMaterial)->textureSource; + if (texture) { + texture->bind(); + } +} diff --git a/src/scenegraph/shadowedbordertexturematerial.h b/src/scenegraph/shadowedbordertexturematerial.h new file mode 100644 index 0000000..791a3e4 --- /dev/null +++ b/src/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() 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 initialize() override; + void updateState(const QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; +}; diff --git a/src/scenegraph/shadowedrectanglematerial.cpp b/src/scenegraph/shadowedrectanglematerial.cpp new file mode 100644 index 0000000..70829ef --- /dev/null +++ b/src/scenegraph/shadowedrectanglematerial.cpp @@ -0,0 +1,107 @@ +/* + * 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() 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")); +} + +const char *const *ShadowedRectangleShader::attributeNames() const +{ + static char const *const names[] = {"in_vertex", "in_uv", nullptr}; + return names; +} + +void ShadowedRectangleShader::initialize() +{ + QSGMaterialShader::initialize(); + m_matrixLocation = program()->uniformLocation("matrix"); + m_aspectLocation = program()->uniformLocation("aspect"); + m_opacityLocation = program()->uniformLocation("opacity"); + m_sizeLocation = program()->uniformLocation("size"); + m_radiusLocation = program()->uniformLocation("radius"); + m_colorLocation = program()->uniformLocation("color"); + m_shadowColorLocation = program()->uniformLocation("shadowColor"); + m_offsetLocation = program()->uniformLocation("offset"); +} + +void ShadowedRectangleShader::updateState(const QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) +{ + auto p = program(); + + if (state.isMatrixDirty()) { + p->setUniformValue(m_matrixLocation, state.combinedMatrix()); + } + + if (state.isOpacityDirty()) { + p->setUniformValue(m_opacityLocation, state.opacity()); + } + + if (!oldMaterial || newMaterial->compare(oldMaterial) != 0 || state.isCachedMaterialDataDirty()) { + auto material = static_cast(newMaterial); + p->setUniformValue(m_aspectLocation, material->aspect); + p->setUniformValue(m_sizeLocation, material->size); + p->setUniformValue(m_radiusLocation, material->radius); + p->setUniformValue(m_colorLocation, material->color); + p->setUniformValue(m_shadowColorLocation, material->shadowColor); + p->setUniformValue(m_offsetLocation, material->offset); + } +} + +void ShadowedRectangleShader::setShader(ShadowedRectangleMaterial::ShaderType shaderType, const QString &shader) +{ + auto header = QOpenGLContext::currentContext()->isOpenGLES() ? QStringLiteral("header_es.glsl") : QStringLiteral("header_desktop.glsl"); + + auto shaderRoot = QStringLiteral(":/org/kde/kirigami/shaders/"); + + setShaderSourceFiles(QOpenGLShader::Vertex, {shaderRoot + header, shaderRoot + QStringLiteral("shadowedrectangle.vert")}); + + QString shaderFile = shader + QStringLiteral(".frag"); + auto sdfFile = QStringLiteral("sdf.glsl"); + if (shaderType == ShadowedRectangleMaterial::ShaderType::LowPower) { + shaderFile = shader + QStringLiteral("_lowpower.frag"); + sdfFile = QStringLiteral("sdf_lowpower.glsl"); + } + + setShaderSourceFiles(QOpenGLShader::Fragment, {shaderRoot + header, shaderRoot + sdfFile, shaderRoot + shaderFile}); +} diff --git a/src/scenegraph/shadowedrectanglematerial.h b/src/scenegraph/shadowedrectanglematerial.h new file mode 100644 index 0000000..bb5bc2f --- /dev/null +++ b/src/scenegraph/shadowedrectanglematerial.h @@ -0,0 +1,66 @@ +/* + * 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() 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); + + char const *const *attributeNames() const override; + + void initialize() override; + void updateState(const QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; + +protected: + void setShader(ShadowedRectangleMaterial::ShaderType shaderType, const QString &shader); + +private: + int m_matrixLocation = -1; + int m_opacityLocation = -1; + int m_aspectLocation = -1; + int m_sizeLocation = -1; + int m_radiusLocation = -1; + int m_colorLocation = -1; + int m_shadowColorLocation = -1; + int m_offsetLocation = -1; +}; diff --git a/src/scenegraph/shadowedrectanglenode.cpp b/src/scenegraph/shadowedrectanglenode.cpp new file mode 100644 index 0000000..ee1e58e --- /dev/null +++ b/src/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/scenegraph/shadowedrectanglenode.h b/src/scenegraph/shadowedrectanglenode.h new file mode 100644 index 0000000..755f56f --- /dev/null +++ b/src/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/scenegraph/shadowedtexturematerial.cpp b/src/scenegraph/shadowedtexturematerial.cpp new file mode 100644 index 0000000..a3d2224 --- /dev/null +++ b/src/scenegraph/shadowedtexturematerial.cpp @@ -0,0 +1,65 @@ +/* + * 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() 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 result; +} + +ShadowedTextureShader::ShadowedTextureShader(ShadowedRectangleMaterial::ShaderType shaderType) + : ShadowedRectangleShader(shaderType) +{ + setShader(shaderType, QStringLiteral("shadowedtexture")); +} + +void ShadowedTextureShader::initialize() +{ + ShadowedRectangleShader::initialize(); + program()->setUniformValue("textureSource", 0); +} + +void ShadowedTextureShader::updateState(const QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) +{ + ShadowedRectangleShader::updateState(state, newMaterial, oldMaterial); + + auto texture = static_cast(newMaterial)->textureSource; + if (texture) { + texture->bind(); + } +} diff --git a/src/scenegraph/shadowedtexturematerial.h b/src/scenegraph/shadowedtexturematerial.h new file mode 100644 index 0000000..453b68b --- /dev/null +++ b/src/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() 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 initialize() override; + void updateState(const QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; +}; diff --git a/src/scenegraph/shadowedtexturenode.cpp b/src/scenegraph/shadowedtexturenode.cpp new file mode 100644 index 0000000..2a66c1c --- /dev/null +++ b/src/scenegraph/shadowedtexturenode.cpp @@ -0,0 +1,81 @@ +/* + * 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); +} + +void ShadowedTextureNode::setTextureSource(QSGTextureProvider *source) +{ + if (m_textureSource == source) { + return; + } + + if (m_textureSource) { + m_textureSource->disconnect(); + } + + m_textureSource = source; + 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/scenegraph/shadowedtexturenode.h b/src/scenegraph/shadowedtexturenode.h new file mode 100644 index 0000000..87d9a43 --- /dev/null +++ b/src/scenegraph/shadowedtexturenode.h @@ -0,0 +1,42 @@ +/* + * 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(); + + void setTextureSource(QSGTextureProvider *source); + void preprocess() override; + +private: + ShadowedRectangleMaterial *createBorderlessMaterial() override; + ShadowedBorderRectangleMaterial *createBorderMaterial() override; + QSGMaterialType *borderlessMaterialType() override; + QSGMaterialType *borderMaterialType() override; + + QPointer m_textureSource; +}; diff --git a/src/scenepositionattached.cpp b/src/scenepositionattached.cpp new file mode 100644 index 0000000..5576a99 --- /dev/null +++ b/src/scenepositionattached.cpp @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: 2017 Marco Martin + * + * 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() +{ +} + +int ScenePositionAttached::x() const +{ + qreal x = 0; + QQuickItem *item = m_item; + + while (item) { + x += item->x(); + item = item->parentItem(); + } + + return x; +} + +int ScenePositionAttached::y() const +{ + qreal y = 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]() { + do { + disconnect(ancestor, nullptr, this, nullptr); + m_ancestors.pop_back(); + } while (!m_ancestors.isEmpty() && m_ancestors.last() != ancestor); + + 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..e17d8ab --- /dev/null +++ b/src/scenepositionattached.h @@ -0,0 +1,62 @@ +/* + * 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 2.5 as Kirigami + * Text { + * text: ScenePosition.x + * } + * @endcode + * @since 2.3 + */ +class ScenePositionAttached : public QObject +{ + Q_OBJECT + /** + * The global scene X position + */ + Q_PROPERTY(int x READ x NOTIFY xChanged) + + /** + * The global scene Y position + */ + Q_PROPERTY(int y READ y NOTIFY yChanged) + +public: + explicit ScenePositionAttached(QObject *parent = nullptr); + ~ScenePositionAttached() override; + + int x() const; + int 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/settings.cpp b/src/settings.cpp new file mode 100644 index 0000000..0aeae38 --- /dev/null +++ b/src/settings.cpp @@ -0,0 +1,238 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "settings.h" + +#include +#include +#include +#include +#include +#include +#include +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#include +#else +#include +#endif +#include + +#include +#include +#include + +#include "libkirigami/tabletmodewatcher.h" + +#ifndef KIRIGAMI_BUILD_TYPE_STATIC +#include "../kirigami_version.h" +#endif + +class SettingsSingleton +{ +public: + Settings self; +}; + +Q_GLOBAL_STATIC(SettingsSingleton, privateSettingsSelf) + +Settings::Settings(QObject *parent) + : QObject(parent) + , m_hasTouchScreen(false) + , m_hasTransientTouchInput(false) +{ + m_tabletModeAvailable = Kirigami::TabletModeWatcher::self()->isTabletModeAvailable(); + connect(Kirigami::TabletModeWatcher::self(), &Kirigami::TabletModeWatcher::tabletModeAvailableChanged, this, [this](bool tabletModeAvailable) { + setTabletModeAvailable(tabletModeAvailable); + }); + + m_tabletMode = Kirigami::TabletModeWatcher::self()->isTabletMode(); + connect(Kirigami::TabletModeWatcher::self(), &Kirigami::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; + } + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + const auto touchDevices = QTouchDevice::devices(); + const auto touchDeviceType = QTouchDevice::TouchScreen; +#else + const auto touchDevices = QInputDevice::devices(); + const auto touchDeviceType = QInputDevice::DeviceType::TouchScreen; +#endif + 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()); + } else { + m_scrollLines = 3; + } +} + +Settings::~Settings() +{ +} + +Settings *Settings::self() +{ + return &privateSettingsSelf()->self; +} + +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; +} + +QStringList Settings::information() const +{ + return { +#ifndef KIRIGAMI_BUILD_TYPE_STATIC + tr("KDE Frameworks %1").arg(QStringLiteral(KIRIGAMI2_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; +} diff --git a/src/settings.h b/src/settings.h new file mode 100644 index 0000000..cf7d7d2 --- /dev/null +++ b/src/settings.h @@ -0,0 +1,137 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ +#ifndef SETTINGS_H +#define SETTINGS_H + +#include +#include + +/** + * This class contains global kirigami settings about the current device setup + * It is exposed to QML as the singleton "Settings" + */ +class Settings : public QObject +{ + Q_OBJECT + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + // 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) + + /** + * 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) + + /** + * 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) + +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; + + QStringList information() const; + + QVariant applicationWindowIcon() const; + + static Settings *self(); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + +Q_SIGNALS: + void tabletModeAvailableChanged(); + void tabletModeChanged(); + void isMobileChanged(); + void hasTransientTouchInputChanged(); + +private: + QString m_style; + int m_scrollLines = 0; + 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/shadowedrectangle.cpp b/src/shadowedrectangle.cpp new file mode 100644 index 0000000..e70c2b7 --- /dev/null +++ b/src/shadowedrectangle.cpp @@ -0,0 +1,367 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "shadowedrectangle.h" + +#include +#include +#include + +#include "scenegraph/paintedrectangleitem.h" +#if QT_CONFIG(opengl) && !defined(KF6_PORTING_TODO) +#include "scenegraph/shadowedrectanglenode.h" +#endif + +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 +{ +#ifndef KF6_PORTING_TODO + return (window() && window()->rendererInterface()->graphicsApi() == QSGRendererInterface::Software) || m_renderType == RenderType::Software; +#else + return true; +#endif +} + +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(); + } +} + +QSGNode *ShadowedRectangle::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data) +{ + Q_UNUSED(data); + +#if QT_CONFIG(opengl) && !defined(KF6_PORTING_TODO) + 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; +#else + Q_UNUSED(node) + return nullptr; +#endif +} + +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); + } +} diff --git a/src/shadowedrectangle.h b/src/shadowedrectangle.h new file mode 100644 index 0000000..efee993 --- /dev/null +++ b/src/shadowedrectangle.h @@ -0,0 +1,321 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include + +class PaintedRectangleItem; + +/** + * Grouped property for rectangle border. + */ +class BorderGroup : public QObject +{ + Q_OBJECT + /** + * The width of the border in pixels. + * + * Default is 0. + */ + Q_PROPERTY(qreal width READ width WRITE setWidth NOTIFY changed) + /** + * The color of the border. + * + * Full RGBA colors are supported. The default is fully opaque black. + */ + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY changed) + +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; +}; + +/** + * Grouped property for rectangle shadow. + */ +class ShadowGroup : public QObject +{ + Q_OBJECT + /** + * The size of the shadow. + * + * This is the approximate size of the shadow in pixels. However, due to falloff + * the actual shadow size can differ. The default is 0, which means no shadow will + * be rendered. + */ + Q_PROPERTY(qreal size READ size WRITE setSize NOTIFY changed) + /** + * Offset of the shadow on the X axis. + * + * In pixels. The default is 0. + */ + Q_PROPERTY(qreal xOffset READ xOffset WRITE setXOffset NOTIFY changed) + /** + * Offset of the shadow on the Y axis. + * + * In pixels. The default is 0. + */ + Q_PROPERTY(qreal yOffset READ yOffset WRITE setYOffset NOTIFY changed) + /** + * The color of the shadow. + * + * Full RGBA colors are supported. The default is fully opaque black. + */ + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY changed) + +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; +}; + +/** + * Grouped property for corner radius. + */ +class CornersGroup : public QObject +{ + Q_OBJECT + /** + * The radius of the top-left corner. + * + * In pixels. Defaults to -1, which indicates this value should not be used. + */ + Q_PROPERTY(qreal topLeftRadius READ topLeft WRITE setTopLeft NOTIFY changed) + /** + * The radius of the top-right corner. + * + * In pixels. Defaults to -1, which indicates this value should not be used. + */ + Q_PROPERTY(qreal topRightRadius READ topRight WRITE setTopRight NOTIFY changed) + /** + * The radius of the bottom-left corner. + * + * In pixels. Defaults to -1, which indicates this value should not be used. + */ + Q_PROPERTY(qreal bottomLeftRadius READ bottomLeft WRITE setBottomLeft NOTIFY changed) + /** + * The radius of the bottom-right corner. + * + * In pixels. Defaults to -1, which indicates this value should not be used. + */ + Q_PROPERTY(qreal bottomRightRadius READ bottomRight WRITE setBottomRight NOTIFY changed) + +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; +}; + +/** + * A rectangle with a shadow. + * + * 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 / 2.12 + */ +class ShadowedRectangle : public QQuickItem +{ + Q_OBJECT + /** + * This property holds the corner radius of the rectangle. + * + * This is the amount of rounding to apply to all of the rectangle's + * corners, in pixels. Individual corners can have a different radius, see + * \property corners. + * + * The default is 0. + */ + Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged) + /** + * This property holds the color of the rectangle. + * + * Full RGBA colors are supported. The default is fully opaque white. + */ + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) + /** + * This property holds the border's properties. + * + * @code + * Kirigami.ShadowedRectangle { + * border.width: 2 + * border.color: Kirigami.Theme.textColor + * } + * @endcode + * \sa BorderGroup + */ + Q_PROPERTY(BorderGroup *border READ border CONSTANT) + /** + * This property holds the shadow's properties. + * + * @code + * Kirigami.ShadowedRectangle { + * shadow.size: 20 + * shadow.xOffset: 5 + * shadow.yOffset: 5 + * } + * @endcode + * + * \sa ShadowGroup + */ + Q_PROPERTY(ShadowGroup *shadow READ shadow CONSTANT) + /** + * Corner radius. + * + * Note that the values from this group override \property radius for the + * corner they affect. + * @code + * Kirigami.ShadowedRectangle { + * corners.topLeftRadius: 4 + * corners.topRightRadius: 5 + * corners.bottomLeftRadius: 2 + * corners.bottomRightRadius: 10 + * @endcode + * + * \sa CornersGroup + */ + Q_PROPERTY(CornersGroup *corners READ corners CONSTANT) + + /** + * This property holds the render type of the rectangle and shadow. + * + * The default is RenderType::Auto. + */ + Q_PROPERTY(RenderType renderType READ renderType WRITE setRenderType CONSTANT) + + Q_PROPERTY(bool softwareRendering READ isSoftwareRendering NOTIFY softwareRenderingChanged) +public: + ShadowedRectangle(QQuickItem *parent = nullptr); + ~ShadowedRectangle() override; + + /** + * Available rendering types for ShadowedRectangle. + */ + enum RenderType { + /** + * 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, + /** + * Use the highest rendering quality possible, even if the hardware might + * not be able to handle it normally. + */ + HighQuality, + /** + * 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, + /** + * 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/shadowedtexture.cpp b/src/shadowedtexture.cpp new file mode 100644 index 0000000..aacf6ff --- /dev/null +++ b/src/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 + +#if QT_CONFIG(opengl) && !defined(KF6_PORTING_TODO) +#include "scenegraph/shadowedtexturenode.h" +#endif + +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 QT_CONFIG(opengl) && !defined(KF6_PORTING_TODO) + + 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; +#else + Q_UNUSED(node) + return nullptr; +#endif +} diff --git a/src/shadowedtexture.h b/src/shadowedtexture.h new file mode 100644 index 0000000..cfb316d --- /dev/null +++ b/src/shadowedtexture.h @@ -0,0 +1,45 @@ +/* + * 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 + + /** + * This property holds the source item that will get rendered with the + * shadow. + */ + Q_PROPERTY(QQuickItem *source READ source WRITE setSource NOTIFY sourceChanged) + +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/sizegroup.cpp b/src/sizegroup.cpp new file mode 100644 index 0000000..d48e5cc --- /dev/null +++ b/src/sizegroup.cpp @@ -0,0 +1,135 @@ +/* + * 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); +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +int SizeGroup::itemCount(QQmlListProperty *prop) +#else +qsizetype SizeGroup::itemCount(QQmlListProperty *prop) +#endif +{ + return pThis->m_items.count(); +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +QQuickItem *SizeGroup::itemAt(QQmlListProperty *prop, int index) +#else +QQuickItem *SizeGroup::itemAt(QQmlListProperty *prop, qsizetype index) +#endif +{ + 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; + } + } +} diff --git a/src/sizegroup.h b/src/sizegroup.h new file mode 100644 index 0000000..19eb408 --- /dev/null +++ b/src/sizegroup.h @@ -0,0 +1,79 @@ +/* + * 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 + 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) + Q_SIGNAL void modeChanged(); + + /** + * Which items this SizeGroup should adjust + */ + Q_PROPERTY(QQmlListProperty items READ items CONSTANT) + 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); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + static int itemCount(QQmlListProperty *prop); + static QQuickItem *itemAt(QQmlListProperty *prop, int index); +#else + static qsizetype itemCount(QQmlListProperty *prop); + static QQuickItem *itemAt(QQmlListProperty *prop, qsizetype index); +#endif + static void clearItems(QQmlListProperty *prop); +}; diff --git a/src/spellcheckinghint.cpp b/src/spellcheckinghint.cpp new file mode 100644 index 0000000..843aa59 --- /dev/null +++ b/src/spellcheckinghint.cpp @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "spellcheckinghint.h" +#include + +SpellCheckingAttached::SpellCheckingAttached(QObject *parent) + : QObject(parent) +{ +} + +SpellCheckingAttached::~SpellCheckingAttached() +{ +} + +void SpellCheckingAttached::setEnabled(bool enabled) +{ + if (enabled == m_enabled) { + return; + } + + m_enabled = enabled; + Q_EMIT enabledChanged(); +} + +bool SpellCheckingAttached::enabled() const +{ + return m_enabled; +} + +SpellCheckingAttached *SpellCheckingAttached::qmlAttachedProperties(QObject *object) +{ + return new SpellCheckingAttached(object); +} diff --git a/src/spellcheckinghint.h b/src/spellcheckinghint.h new file mode 100644 index 0000000..3420027 --- /dev/null +++ b/src/spellcheckinghint.h @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include +#include + +/** + * @brief This attached property contains hints for enabling spell checking. + * + * @warning Kirigami doesn't provide the spell checking, this is just an hint + * for the theme. If you want to add spell checking to your custom application + * theme checkout \ref Sonnet. + * + * @code + * import org.kde.kirigami 2.18 as Kirigami + * TextArea { + * Kirigami.SpellChecking.enabled: true + * } + * @endcode + * @author Carl Schwan + * @since 2.18 + */ +class SpellCheckingAttached : public QObject +{ + Q_OBJECT + /** + * 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 + * spellchecking on your application, explicitly set it to false. + * + * @since 2.18 + */ + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) +public: + explicit SpellCheckingAttached(QObject *parent = nullptr); + ~SpellCheckingAttached() override; + + void setEnabled(bool enabled); + bool enabled() const; + + // QML attached property + static SpellCheckingAttached *qmlAttachedProperties(QObject *object); + +Q_SIGNALS: + void enabledChanged(); + +private: + bool m_enabled = false; +}; + +QML_DECLARE_TYPEINFO(SpellCheckingAttached, QML_HAS_ATTACHED_PROPERTIES) diff --git a/src/styles/Material/AbstractListItem.qml b/src/styles/Material/AbstractListItem.qml new file mode 100644 index 0000000..0ea7d85 --- /dev/null +++ b/src/styles/Material/AbstractListItem.qml @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.7 +import org.kde.kirigami 2.4 +import QtQuick.Controls.Material 2.1 as Mat +import QtQuick.Controls.Material.impl 2.1 as MatImp +import "../../private" +import "../../templates" as T + +T.AbstractListItem { + id: listItem + + background: DefaultListItemBackground { + + MatImp.Ripple { + anchors.fill: parent + clip: visible + visible: listItem.supportsMouseEvents + pressed: listItem.pressed + anchor: listItem + active: listItem.down || listItem.visualFocus + color: Qt.rgba(0,0,0,0.2) + } + } + implicitHeight: contentItem.implicitHeight + Units.smallSpacing * 6 +} diff --git a/src/styles/Material/InlineMessage.qml b/src/styles/Material/InlineMessage.qml new file mode 100644 index 0000000..e557d17 --- /dev/null +++ b/src/styles/Material/InlineMessage.qml @@ -0,0 +1,115 @@ +/* + * 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 2.7 +import QtGraphicalEffects 1.0 +import org.kde.kirigami 2.5 as Kirigami + +import "../../private" +import "../../templates" as T + +/** + * 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 + * InlineMessage { + * type: Kirigami.MessageType.Error + * + * text: "My error message" + * + * actions: [ + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * }, + * Kirigami.Action { + * icon.name: "edit" + * text: "Action text" + * onTriggered: { + * // do stuff + * } + * } + * ] + * } + * @endcode + * + * @since 5.45 + */ + +T.InlineMessage { + id: root + + background: Rectangle { + id: bgBorderRect + + color: { + if (root.type == Kirigami.MessageType.Positive) { + return Kirigami.Theme.positiveTextColor; + } else if (root.type == Kirigami.MessageType.Warning) { + return Kirigami.Theme.neutralTextColor; + } else if (root.type == Kirigami.MessageType.Error) { + return Kirigami.Theme.negativeTextColor; + } + + return Kirigami.Theme.activeTextColor; + } + + radius: Kirigami.Units.smallSpacing / 2 + + Rectangle { + id: bgFillRect + + anchors.fill: parent + anchors.margins: 1 + + color: Kirigami.Theme.backgroundColor + + radius: bgBorderRect.radius * 0.60 + } + + Rectangle { + anchors.fill: bgFillRect + + color: bgBorderRect.color + + opacity: 0.20 + + radius: bgFillRect.radius + } + + layer.enabled: true + layer.effect: DropShadow { + horizontalOffset: 0 + verticalOffset: 1 + radius: 12 + samples: 32 + color: Qt.rgba(0, 0, 0, 0.5) + } + } +} diff --git a/src/styles/Material/Label.qml b/src/styles/Material/Label.qml new file mode 100644 index 0000000..24290e6 --- /dev/null +++ b/src/styles/Material/Label.qml @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2011 by Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Window 2.2 +import org.kde.kirigami 2.4 +import QtQuick.Controls 2.0 as Controls + +/** + * This is a label which uses the current Theme. + * + * The characteristics of the text will be automatically set according to the + * current Theme. If you need a more customized text item use the Text component + * from QtQuick. + * + * You can use all elements of the QML Text component, in particular the "text" + * property to define the label text. + * + * @inherit QtQuick.Templates.Label + * @deprecated use QtQuick.Templates.Label directly, it will be styled appropriately + */ +Controls.Label { + verticalAlignment: lineCount > 1 ? Text.AlignTop : Text.AlignVCenter + + activeFocusOnTab: false + + Component.onCompleted: { + console.warn("Kirigami.Label is deprecated. Use QtQuickControls2.Label instead") + } +} diff --git a/src/styles/Material/SwipeListItem.qml b/src/styles/Material/SwipeListItem.qml new file mode 100644 index 0000000..722e27c --- /dev/null +++ b/src/styles/Material/SwipeListItem.qml @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2010 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import org.kde.kirigami 2.4 +import QtQuick.Controls.Material 2.1 as Mat +import QtQuick.Controls.Material.impl 2.1 as MatImp +import "../../private" +import "../../templates" as T + +/** + * 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. + * Any subclass of AbstractListItem can be assigned as the contentItem property. + * @code + * ListView { + * model: myModel + * delegate: SwipeListItem { + * 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.Item + */ +T.SwipeListItem { + id: listItem + + background: DefaultListItemBackground { + MatImp.Ripple { + anchors.fill: parent + clip: visible + pressed: listItem.pressed + anchor: listItem + active: listItem.down || listItem.visualFocus + color: Qt.rgba(0,0,0,0.2) + } + } + implicitHeight: contentItem.implicitHeight + Units.smallSpacing * 6 +} diff --git a/src/styles/Material/Theme.qml b/src/styles/Material/Theme.qml new file mode 100644 index 0000000..fa87f4b --- /dev/null +++ b/src/styles/Material/Theme.qml @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.7 +import QtQuick.Controls.Material 2.0 +import org.kde.kirigami 2.16 as Kirigami + +/** + * \internal + */ +Kirigami.BasicThemeDefinition { + id: theme + //NOTE: this is useless per se, but it forces the Material attached property to be created + Material.elevation:2 + + textColor: theme.Material.foreground + disabledTextColor: "#9931363b" + + highlightColor: theme.Material.accent + //FIXME: something better? + highlightedTextColor: theme.Material.background + backgroundColor: theme.Material.background + alternateBackgroundColor: Qt.darker(theme.Material.background, 1.05) + + hoverColor: theme.Material.highlightedButtonColor + focusColor: theme.Material.highlightedButtonColor + + activeTextColor: theme.Material.primary + activeBackgroundColor: theme.Material.primary + linkColor: "#2980B9" + linkBackgroundColor: "#2980B9" + visitedLinkColor: "#7F8C8D" + visitedLinkBackgroundColor: "#7F8C8D" + negativeTextColor: "#DA4453" + negativeBackgroundColor: "#DA4453" + neutralTextColor: "#F67400" + neutralBackgroundColor: "#F67400" + positiveTextColor: "#27AE60" + positiveBackgroundColor: "#27AE60" + + buttonTextColor: theme.Material.foreground + buttonBackgroundColor: theme.Material.buttonColor + buttonAlternateBackgroundColor: Qt.darker(theme.Material.buttonColor, 1.05) + buttonHoverColor: theme.Material.highlightedButtonColor + buttonFocusColor: theme.Material.highlightedButtonColor + + viewTextColor: theme.Material.foreground + viewBackgroundColor: theme.Material.dialogColor + viewAlternateBackgroundColor: Qt.darker(theme.Material.dialogColor, 1.05) + viewHoverColor: theme.Material.listHighlightColor + viewFocusColor: theme.Material.listHighlightColor + + selectionTextColor: theme.Material.primaryHighlightedTextColor + selectionBackgroundColor: theme.Material.textSelectionColor + selectionAlternateBackgroundColor: Qt.darker(theme.Material.textSelectionColor, 1.05) + selectionHoverColor: theme.Material.highlightedButtonColor + selectionFocusColor: theme.Material.highlightedButtonColor + + tooltipTextColor: fontMetrics.Material.foreground + tooltipBackgroundColor: fontMetrics.Material.tooltipColor + tooltipAlternateBackgroundColor: Qt.darker(theme.Material.tooltipColor, 1.05) + tooltipHoverColor: fontMetrics.Material.highlightedButtonColor + tooltipFocusColor: fontMetrics.Material.highlightedButtonColor + + complementaryTextColor: fontMetrics.Material.foreground + complementaryBackgroundColor: fontMetrics.Material.background + complementaryAlternateBackgroundColor: Qt.lighter(fontMetrics.Material.background, 1.05) + complementaryHoverColor: theme.Material.highlightedButtonColor + complementaryFocusColor: theme.Material.highlightedButtonColor + + headerTextColor: fontMetrics.Material.primaryTextColor + headerBackgroundColor: fontMetrics.Material.primaryColor + headerAlternateBackgroundColor: Qt.lighter(fontMetrics.Material.primaryColor, 1.05) + headerHoverColor: theme.Material.highlightedButtonColor + headerFocusColor: theme.Material.highlightedButtonColor + + defaultFont: fontMetrics.font + + property list children: [ + TextMetrics { + id: fontMetrics + //this is to get a source of dark colors + Material.theme: Material.Dark + } + ] + + onSync: { + //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..4d8289b --- /dev/null +++ b/src/styles/org.kde.desktop/AbstractApplicationHeader.qml @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import org.kde.kirigami 2.4 + +import "../../templates" as T + + +/** + * 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 + */ +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 + Theme.inherit: false + Theme.colorSet: Theme.Header + + background: Rectangle { + color: Theme.backgroundColor + 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/AbstractListItem.qml b/src/styles/org.kde.desktop/AbstractListItem.qml new file mode 100644 index 0000000..a32d03e --- /dev/null +++ b/src/styles/org.kde.desktop/AbstractListItem.qml @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import org.kde.kirigami 2.4 +import "../../private" +import "../../templates" as T + +T.AbstractListItem { + id: listItem + + background: DefaultListItemBackground {} +} diff --git a/src/styles/org.kde.desktop/SwipeListItem.qml b/src/styles/org.kde.desktop/SwipeListItem.qml new file mode 100644 index 0000000..15f4175 --- /dev/null +++ b/src/styles/org.kde.desktop/SwipeListItem.qml @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import org.kde.kirigami 2.4 +import "../../private" +import "../../templates" as T + +T.SwipeListItem { + id: listItem + + background: DefaultListItemBackground {} +} diff --git a/src/styles/org.kde.desktop/Theme.qml b/src/styles/org.kde.desktop/Theme.qml new file mode 100644 index 0000000..a6e8324 --- /dev/null +++ b/src/styles/org.kde.desktop/Theme.qml @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.4 +import org.kde.kirigami 2.16 as Kirigami + +pragma Singleton + +Kirigami.BasicThemeDefinition { + id: theme + + textColor: palette.windowText + disabledTextColor: disabledPalette.windowText + + highlightColor: palette.highlight + highlightedTextColor: palette.highlightedText + backgroundColor: palette.window + alternateBackgroundColor: Qt.darker(palette.window, 1.05) + activeTextColor: palette.highlight + 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.base + tooltipBackgroundColor: palette.text + tooltipAlternateBackgroundColor: Qt.darker(palette.text, 1.05) + tooltipHoverColor: palette.highlight + tooltipFocusColor: palette.highlight + + complementaryTextColor: palette.base + complementaryBackgroundColor: palette.text + complementaryAlternateBackgroundColor: Qt.darker(palette.text, 1.05) + complementaryHoverColor: palette.highlight + complementaryFocusColor: palette.highlight + + headerTextColor: palette.text + headerBackgroundColor: palette.base + headerAlternateBackgroundColor: palette.alternateBase + headerHoverColor: palette.highlight + headerFocusColor: palette.highlight + + property font 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/toolbarlayout.cpp b/src/toolbarlayout.cpp new file mode 100644 index 0000000..6a84371 --- /dev/null +++ b/src/toolbarlayout.cpp @@ -0,0 +1,713 @@ +/* + * 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 "enums.h" +#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 ToolBarLayout::Private +{ +public: + Private(ToolBarLayout *qq) + : q(qq) + { + } + + void performLayout(); + QVector createDelegates(); + ToolBarLayoutDelegate *createDelegate(QObject *action); + qreal layoutStart(qreal layoutWidth); + void maybeHideDelegate(int index, qreal ¤tWidth, qreal totalWidth); + + ToolBarLayout *q; + + QVector actions; + ActionsProperty actionsProperty; + QList hiddenActions; + QQmlComponent *fullDelegate = nullptr; + QQmlComponent *iconDelegate = nullptr; + QQmlComponent *moreButton = nullptr; + qreal spacing = 0.0; + Qt::Alignment alignment = Qt::AlignLeft; + qreal visibleWidth = 0.0; + Qt::LayoutDirection layoutDirection = Qt::LeftToRight; + HeightMode heightMode = ConstrainIfLarger; + + bool completed = false; + bool layoutQueued = false; + bool actionsChanged = false; + std::unordered_map> delegates; + QVector sortedDelegates; + QQuickItem *moreButtonInstance = nullptr; + ToolBarDelegateIncubator *moreButtonIncubator = nullptr; + bool shouldShowMoreButton = false; + int firstHiddenIndex = -1; + + QVector removedActions; + QTimer *removalTimer = nullptr; + + QElapsedTimer performanceTimer; + + static void appendAction(ToolBarLayout::ActionsProperty *list, QObject *action); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + static int actionCount(ToolBarLayout::ActionsProperty *list); + static QObject *action(ToolBarLayout::ActionsProperty *list, int index); +#else + static qsizetype actionCount(ToolBarLayout::ActionsProperty *list); + static QObject *action(ToolBarLayout::ActionsProperty *list, qsizetype index); +#endif + static void clearActions(ToolBarLayout::ActionsProperty *list); +}; + +ToolBarLayout::ToolBarLayout(QQuickItem *parent) + : QQuickItem(parent) + , d(std::make_unique(this)) +{ + d->actionsProperty = ActionsProperty(this, this, Private::appendAction, Private::actionCount, Private::action, Private::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) +{ + 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::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() +{ + if (d->completed) { + polish(); + } +} + +void ToolBarLayout::componentComplete() +{ + QQuickItem::componentComplete(); + d->completed = true; + relayout(); +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +void ToolBarLayout::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +#else +void ToolBarLayout::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +#endif +{ + if (newGeometry != oldGeometry) { + relayout(); + } +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QQuickItem::geometryChanged(newGeometry, oldGeometry); +#else + QQuickItem::geometryChange(newGeometry, oldGeometry); +#endif +} + +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(); +} + +void ToolBarLayout::Private::performLayout() +{ + if (!fullDelegate || !iconDelegate || !moreButton) { + qCWarning(KirigamiLog) << "ToolBarLayout: Unable to layout, required properties are not set"; + return; + } + + if (actions.isEmpty()) { + q->setImplicitWidth(0); + q->setImplicitHeight(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 = moreButtonInstance->isVisible() ? moreButtonInstance->height() : 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; + + if (q->heightValid() && q->height() > 0.0) { + maxHeight = q->height(); + } + + qreal 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()) { + if (layoutDirection == Qt::LeftToRight) { + moreButtonInstance->setX(q->width() - moreButtonInstance->width()); + } else { + moreButtonInstance->setX(0.0); + } + + if (heightMode == AlwaysFill) { + moreButtonInstance->setHeight(q->height()); + } else if (heightMode == ConstrainIfLarger) { + if (moreButtonInstance->implicitHeight() > maxHeight) { + moreButtonInstance->setHeight(maxHeight); + } else { + moreButtonInstance->setHeight(moreButtonInstance->implicitHeight()); + } + } + + moreButtonInstance->setY(qRound((maxHeight - 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 == AlwaysFill) { + entry->setHeight(q->height()); + } else if (heightMode == ConstrainIfLarger) { + if (entry->implicitHeight() > maxHeight) { + entry->setHeight(maxHeight); + } else { + entry->setHeight(entry->implicitHeight()); + } + } + + qreal y = qRound((maxHeight - 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(); + } + + q->setImplicitSize(maxWidth, maxHeight); + Q_EMIT q->hiddenActionsChanged(); + + 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(); +} + +QVector ToolBarLayout::Private::createDelegates() +{ + QVector result; + for (auto action : std::as_const(actions)) { + if (delegates.find(action) != delegates.end()) { + result.append(delegates.at(action).get()); + } else { + 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); + + connect(moreButtonInstance, &QQuickItem::visibleChanged, q, [this]() { + moreButtonInstance->setVisible(shouldShowMoreButton); + }); + connect(moreButtonInstance, &QQuickItem::widthChanged, q, [this]() { + Q_EMIT q->minimumWidthChanged(); + }); + q->relayout(); + Q_EMIT q->minimumWidthChanged(); + + QTimer::singleShot(0, q, [this]() { + delete moreButtonIncubator; + moreButtonIncubator = nullptr; + }); + }); + moreButtonIncubator->create(); + } + + return result; +} + +ToolBarLayoutDelegate *ToolBarLayout::Private::createDelegate(QObject *action) +{ + QQmlComponent *fullComponent = nullptr; + auto displayComponent = action->property("displayComponent"); + if (displayComponent.isValid()) { + fullComponent = displayComponent.value(); + } + + if (!fullComponent) { + fullComponent = fullDelegate; + } + + 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); + }); + + return result; +} + +qreal ToolBarLayout::Private::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 ToolBarLayout::Private::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 ToolBarLayout::Private::appendAction(ToolBarLayout::ActionsProperty *list, QObject *action) +{ + auto layout = reinterpret_cast(list->data); + layout->addAction(action); +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +int ToolBarLayout::Private::actionCount(ToolBarLayout::ActionsProperty *list) +#else +qsizetype ToolBarLayout::Private::actionCount(ToolBarLayout::ActionsProperty *list) +#endif +{ + return reinterpret_cast(list->data)->d->actions.count(); +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +QObject *ToolBarLayout::Private::action(ToolBarLayout::ActionsProperty *list, int index) +#else +QObject *ToolBarLayout::Private::action(ToolBarLayout::ActionsProperty *list, qsizetype index) +#endif +{ + return reinterpret_cast(list->data)->d->actions.at(index); +} + +void ToolBarLayout::Private::clearActions(ToolBarLayout::ActionsProperty *list) +{ + reinterpret_cast(list->data)->clearActions(); +} diff --git a/src/toolbarlayout.h b/src/toolbarlayout.h new file mode 100644 index 0000000..1419bbe --- /dev/null +++ b/src/toolbarlayout.h @@ -0,0 +1,233 @@ +/* + * 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 + +/** + * 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) +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 + /** + * The actions this layout should create delegates for. + */ + Q_PROPERTY(QQmlListProperty actions READ actionsProperty NOTIFY actionsChanged) + /** + * A list of actions that do not fit in the current view and are thus hidden. + */ + Q_PROPERTY(QList hiddenActions READ hiddenActions NOTIFY hiddenActionsChanged) + /** + * 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) + /** + * A component that is used to create icon-only delegates from. + * + * \sa fullDelegate + */ + Q_PROPERTY(QQmlComponent *iconDelegate READ iconDelegate WRITE setIconDelegate NOTIFY iconDelegateChanged) + /** + * 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) + /** + * The amount of spacing between individual delegates. + */ + Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) + /** + * 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) + /** + * The combined width of visible delegates in this layout. + */ + Q_PROPERTY(qreal visibleWidth READ visibleWidth NOTIFY visibleWidthChanged) + /** + * The minimum width this layout can have. + * + * This is equal to the width of the moreButton. + */ + Q_PROPERTY(qreal minimumWidth READ minimumWidth NOTIFY minimumWidthChanged) + /** + * 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) + /** + * 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) + +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 *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; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; +#else + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; +#endif + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) override; + void updatePolish() override; + +private: + class Private; + const std::unique_ptr d; +}; + +QML_DECLARE_TYPEINFO(ToolBarLayout, QML_HAS_ATTACHED_PROPERTIES) + +#endif // TOOLBARLAYOUT_H diff --git a/src/toolbarlayoutdelegate.cpp b/src/toolbarlayoutdelegate.cpp new file mode 100644 index 0000000..b143b82 --- /dev/null +++ b/src/toolbarlayoutdelegate.cpp @@ -0,0 +1,325 @@ +/* + * 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(KirigamiLog) << "Could not create delegate for ToolBarLayout"; + const auto e = errors(); + for (const auto &error : e) { + qCWarning(KirigamiLog) << 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(KirigamiLog) << "Could not create delegate for ToolBarLayout"; + const auto errors = incubator->errors(); + for (const auto &error : errors) { + qCWarning(KirigamiLog) << error; + } + return; + } + + m_full = qobject_cast(incubator->object()); + m_full->setVisible(false); + connect(m_full, &QQuickItem::widthChanged, this, [this]() { + m_parent->relayout(); + }); + connect(m_full, &QQuickItem::heightChanged, this, [this]() { + m_parent->relayout(); + }); + 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(KirigamiLog) << "Could not create delegate for ToolBarLayout"; + const auto errors = incubator->errors(); + for (const auto &error : errors) { + qCWarning(KirigamiLog) << error; + } + return; + } + + m_icon = qobject_cast(incubator->object()); + m_icon->setVisible(false); + connect(m_icon, &QQuickItem::widthChanged, this, [this]() { + m_parent->relayout(); + }); + connect(m_icon, &QQuickItem::heightChanged, this, [this]() { + m_parent->relayout(); + }); + 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); +} + +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->height(), m_icon->height()); +} + +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; + } +} diff --git a/src/toolbarlayoutdelegate.h b/src/toolbarlayoutdelegate.h new file mode 100644 index 0000000..106652a --- /dev/null +++ b/src/toolbarlayoutdelegate.h @@ -0,0 +1,111 @@ +/* + * 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 "enums.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); + + 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(); + + 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/wheelhandler.cpp b/src/wheelhandler.cpp new file mode 100644 index 0000000..d1caa31 --- /dev/null +++ b/src/wheelhandler.cpp @@ -0,0 +1,627 @@ +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "wheelhandler.h" +#include "settings.h" +#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); + }); + + 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() = default; + +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); + } + + m_flickable = target; + m_filterItem->setParentItem(target); + + QQuickItem *vscrollbar = nullptr; + QQuickItem *hscrollbar = nullptr; + + 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()); + }); + + // Get ScrollBars so that we can filter them too, even if they're not in the bounds of the Flickable + auto targetChildren = target->children(); + for (auto child : targetChildren) { + if (child->inherits("QQuickScrollBarAttached")) { + vscrollbar = child->property("vertical").value(); + hscrollbar = 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 targetParent = target->parentItem(); + if (targetParent && targetParent->inherits("QQuickScrollView") && !vscrollbar && !hscrollbar) { + auto targetParentChildren = targetParent->children(); + for (auto child : targetParentChildren) { + if (child->inherits("QQuickScrollBarAttached")) { + vscrollbar = child->property("vertical").value(); + hscrollbar = child->property("horizontal").value(); + break; + } + } + } + } + + if (m_verticalScrollBar != vscrollbar) { + if (m_verticalScrollBar) { + m_verticalScrollBar->removeEventFilter(this); + } + m_verticalScrollBar = vscrollbar; + if (vscrollbar) { + vscrollbar->installEventFilter(this); + } + } + + if (m_horizontalScrollBar != hscrollbar) { + if (m_horizontalScrollBar) { + m_horizontalScrollBar->removeEventFilter(this); + } + m_horizontalScrollBar = hscrollbar; + if (hscrollbar) { + hscrollbar->installEventFilter(this); + } + } + + Q_EMIT targetChanged(); +} + +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::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 = qBound(-minYExtent, contentY - yChange, -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; + m_flickable->setProperty("contentY", newContentY); + } + } + + 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); + + // 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; +} diff --git a/src/wheelhandler.h b/src/wheelhandler.h new file mode 100644 index 0000000..d921a6c --- /dev/null +++ b/src/wheelhandler.h @@ -0,0 +1,378 @@ +/* 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 + +class QWheelEvent; +class WheelHandler; + +/** + * Describes the mouse wheel event + */ +class KirigamiWheelEvent : public QObject +{ + Q_OBJECT + + /** + * x: real + * + * X coordinate of the mouse pointer + */ + Q_PROPERTY(qreal x READ x CONSTANT) + + /** + * y: real + * + * Y coordinate of the mouse pointer + */ + Q_PROPERTY(qreal y READ y CONSTANT) + + /** + * 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) + + /** + * pixelDelta: point + * + * provides the delta in screen pixels available on high resolution trackpads + */ + Q_PROPERTY(QPointF pixelDelta READ pixelDelta CONSTANT) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + +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 +{ + Q_OBJECT + + /** + * @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) + + /** + * @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) + +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: + void setScrolling(bool scrolling); + bool scrollFlickable(QPointF pixelDelta, + QPointF angleDelta = {}, + Qt::KeyboardModifiers modifiers = Qt::NoModifier); + + QPointer m_flickable; + QPointer m_verticalScrollBar; + QPointer m_horizontalScrollBar; + 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_wasTouched = 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; +}; diff --git a/templates/CMakeLists.txt b/templates/CMakeLists.txt new file mode 100644 index 0000000..f69b2ac --- /dev/null +++ b/templates/CMakeLists.txt @@ -0,0 +1,3 @@ +set(apptemplate_DIRS kirigami) + +kde_package_app_templates(TEMPLATES ${apptemplate_DIRS} INSTALL_DIR ${KDE_INSTALL_KAPPTEMPLATESDIR}) diff --git a/templates/kirigami/CMakeLists.txt b/templates/kirigami/CMakeLists.txt new file mode 100644 index 0000000..c4c7bf3 --- /dev/null +++ b/templates/kirigami/CMakeLists.txt @@ -0,0 +1,41 @@ +# 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(QT5_MIN_VERSION 5.15) +set(KF5_MIN_VERSION 5.83) + +find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH}) + +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDECompilerSettings NO_POLICY_SCOPE) +include(ECMSetupVersion) +include(ECMGenerateHeaders) +include(ECMPoQmTools) + +ecm_setup_version(${PROJECT_VERSION} + VARIABLE_PREFIX %{APPNAMEUC} + VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/src/version-%{APPNAMELC}.h" +) + +find_package(Qt5 ${QT5_MIN_VERSION} REQUIRED COMPONENTS Core Gui Qml QuickControls2 Svg) +find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Kirigami2 CoreAddons Config I18n) + +if (ANDROID) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle) +endif() + +add_subdirectory(src) + +install(PROGRAMS org.kde.%{APPNAMELC}.desktop DESTINATION ${KDE_INSTALL_APPDIR}) +install(FILES org.kde.%{APPNAMELC}.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) + +feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/templates/kirigami/android/AndroidManifest.xml b/templates/kirigami/android/AndroidManifest.xml new file mode 100644 index 0000000..e99438d --- /dev/null +++ b/templates/kirigami/android/AndroidManifest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/kirigami/android/build.gradle b/templates/kirigami/android/build.gradle new file mode 100644 index 0000000..1882912 --- /dev/null +++ b/templates/kirigami/android/build.gradle @@ -0,0 +1,78 @@ +/* + SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + SPDX-License-Identifier: BSD-3-Clause +*/ + +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.6.4' + } +} + +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 + * - qt5AndroidDir - 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.toInteger() + + buildToolsVersion androidBuildToolsVersion + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java'] + aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl'] + res.srcDirs = [qt5AndroidDir + '/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 + manifestPlaceholders = [versionName: projectVersionFull, versionCode: timestamp] + } + +} + diff --git a/templates/kirigami/android/res/drawable/logo.png b/templates/kirigami/android/res/drawable/logo.png new file mode 100644 index 0000000..a63448d Binary files /dev/null and b/templates/kirigami/android/res/drawable/logo.png differ diff --git a/templates/kirigami/android/res/drawable/splash.xml b/templates/kirigami/android/res/drawable/splash.xml new file mode 100644 index 0000000..d69509e --- /dev/null +++ b/templates/kirigami/android/res/drawable/splash.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/templates/kirigami/android/version.gradle.in b/templates/kirigami/android/version.gradle.in new file mode 100644 index 0000000..9ddd1bc --- /dev/null +++ b/templates/kirigami/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/kirigami/kirigami-app.png b/templates/kirigami/kirigami-app.png new file mode 100644 index 0000000..8df7987 Binary files /dev/null and b/templates/kirigami/kirigami-app.png differ diff --git a/templates/kirigami/kirigami.kdevtemplate b/templates/kirigami/kirigami.kdevtemplate new file mode 100644 index 0000000..986ba46 --- /dev/null +++ b/templates/kirigami/kirigami.kdevtemplate @@ -0,0 +1,8 @@ +# KDE Config File +[General] +Name=Kirigami Application +Comment=Convergent application using Qt Quick Controls 2 and Kirigami + +Category=Qt/Graphical +Icon=kirigami-app.png +ShowFilesAfterGeneration=src/main.cpp diff --git a/templates/kirigami/org.kde.%{APPNAMELC}.desktop b/templates/kirigami/org.kde.%{APPNAMELC}.desktop new file mode 100644 index 0000000..dfe88e9 --- /dev/null +++ b/templates/kirigami/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/kirigami/org.kde.%{APPNAMELC}.json b/templates/kirigami/org.kde.%{APPNAMELC}.json new file mode 100644 index 0000000..8d2a190 --- /dev/null +++ b/templates/kirigami/org.kde.%{APPNAMELC}.json @@ -0,0 +1,28 @@ +{ + "id": "org.kde.%{APPNAMELC}", + "runtime": "org.kde.Platform", + "runtime-version": "5.15", + "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", + "--filesystem=home" + ], + "separate-locales": false, + + "modules": [ + { + "name": "%{APPNAMELC}", + "buildsystem": "cmake-ninja", + "builddir": true, + "sources": [ { "type": "dir", "path": ".", "skip": [".git"] } ] + } + ] +} + diff --git a/templates/kirigami/org.kde.%{APPNAMELC}.metainfo.xml b/templates/kirigami/org.kde.%{APPNAMELC}.metainfo.xml new file mode 100644 index 0000000..795ef61 --- /dev/null +++ b/templates/kirigami/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/kirigami/src/%{APPNAMELC}config.kcfg b/templates/kirigami/src/%{APPNAMELC}config.kcfg new file mode 100644 index 0000000..1c0dac1 --- /dev/null +++ b/templates/kirigami/src/%{APPNAMELC}config.kcfg @@ -0,0 +1,16 @@ + + + + + + + true + + + diff --git a/templates/kirigami/src/%{APPNAMELC}config.kcfgc b/templates/kirigami/src/%{APPNAMELC}config.kcfgc new file mode 100644 index 0000000..f112ed3 --- /dev/null +++ b/templates/kirigami/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/kirigami/src/CMakeLists.txt b/templates/kirigami/src/CMakeLists.txt new file mode 100644 index 0000000..4a0ada6 --- /dev/null +++ b/templates/kirigami/src/CMakeLists.txt @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: BSD-3-Clause +# SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + +add_executable(%{APPNAMELC} + main.cpp + about.cpp + app.cpp + resources.qrc) + +target_link_libraries(%{APPNAMELC} + Qt5::Core + Qt5::Gui + Qt5::Qml + Qt5::Quick + Qt5::QuickControls2 + Qt5::Svg + KF5::I18n + KF5::CoreAddons + KF5::ConfigCore + KF5::ConfigGui) + +if (ANDROID) + kirigami_package_breeze_icons(ICONS + list-add + help-about + application-exit + applications-graphics + ) +endif() + +kconfig_add_kcfg_files(%{APPNAMELC} GENERATE_MOC %{APPNAMELC}config.kcfgc) +install(TARGETS %{APPNAMELC} ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/templates/kirigami/src/LICENSES/BSD-3-Clause.txt b/templates/kirigami/src/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000..0741db7 --- /dev/null +++ b/templates/kirigami/src/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/kirigami/src/LICENSES/CC0-1.0.txt b/templates/kirigami/src/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/templates/kirigami/src/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/kirigami/src/LICENSES/FSFAP.txt b/templates/kirigami/src/LICENSES/FSFAP.txt new file mode 100644 index 0000000..c96c65e --- /dev/null +++ b/templates/kirigami/src/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/kirigami/src/LICENSES/GPL-2.0-or-later.txt b/templates/kirigami/src/LICENSES/GPL-2.0-or-later.txt new file mode 100644 index 0000000..3b6070f --- /dev/null +++ b/templates/kirigami/src/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/kirigami/src/about.cpp b/templates/kirigami/src/about.cpp new file mode 100644 index 0000000..83a7c47 --- /dev/null +++ b/templates/kirigami/src/about.cpp @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// PDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + +#include "about.h" + +KAboutData AboutType::aboutData() const +{ + return KAboutData::applicationData(); +} diff --git a/templates/kirigami/src/about.h b/templates/kirigami/src/about.h new file mode 100644 index 0000000..b403988 --- /dev/null +++ b/templates/kirigami/src/about.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + +#pragma once + +#include +#include + +class AboutType : public QObject +{ + Q_OBJECT + Q_PROPERTY(KAboutData aboutData READ aboutData CONSTANT) +public: + [[nodiscard]] KAboutData aboutData() const; +}; diff --git a/templates/kirigami/src/app.cpp b/templates/kirigami/src/app.cpp new file mode 100644 index 0000000..332ac19 --- /dev/null +++ b/templates/kirigami/src/app.cpp @@ -0,0 +1,24 @@ +// 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(); +} diff --git a/templates/kirigami/src/app.h b/templates/kirigami/src/app.h new file mode 100644 index 0000000..ba49a48 --- /dev/null +++ b/templates/kirigami/src/app.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + +#pragma once + +#include + +class QQuickWindow; + +class App : public QObject +{ + Q_OBJECT + +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/kirigami/src/contents/ui/About.qml b/templates/kirigami/src/contents/ui/About.qml new file mode 100644 index 0000000..ff4cdb2 --- /dev/null +++ b/templates/kirigami/src/contents/ui/About.qml @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.19 as Kirigami +import org.kde.%{APPNAME} 1.0 + +Kirigami.AboutPage { + aboutData: AboutType.aboutData +} diff --git a/templates/kirigami/src/contents/ui/main.qml b/templates/kirigami/src/contents/ui/main.qml new file mode 100644 index 0000000..f2120a7 --- /dev/null +++ b/templates/kirigami/src/contents/ui/main.qml @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.19 as Kirigami +import org.kde.%{APPNAME} 1.0 + +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 { + title: i18n("%{APPNAME}") + titleIcon: "applications-graphics" + isMenu: !root.isMobile + actions: [ + Kirigami.Action { + text: i18n("Plus One") + icon.name: "list-add" + onTriggered: { + counter += 1 + } + }, + Kirigami.Action { + text: i18n("About %{APPNAME}") + icon.name: "help-about" + onTriggered: pageStack.layers.push('qrc: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 + + Layout.fillWidth: true + + title: i18n("Main Page") + + actions.main: Kirigami.Action { + text: i18n("Plus One") + icon.name: "list-add" + tooltip: i18n("Add one to the counter") + onTriggered: { + counter += 1 + } + } + + ColumnLayout { + width: page.width + + anchors.centerIn: parent + + Kirigami.Heading { + Layout.alignment: Qt.AlignCenter + text: counter == 0 ? i18n("Hello, World!") : counter + } + + Controls.Button { + Layout.alignment: Qt.AlignHCenter + text: "+ 1" + onClicked: counter += 1 + } + } + } +} diff --git a/templates/kirigami/src/main.cpp b/templates/kirigami/src/main.cpp new file mode 100644 index 0000000..4109dd7 --- /dev/null +++ b/templates/kirigami/src/main.cpp @@ -0,0 +1,66 @@ +/* + SPDX-License-Identifier: GPL-2.0-or-later + SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> +*/ + +#include +#include +#include +#include + +#include "about.h" +#include "app.h" +#include "version-%{APPNAMELC}.h" +#include +#include +#include + +#include "%{APPNAMELC}config.h" + +Q_DECL_EXPORT int main(int argc, char *argv[]) +{ + QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QApplication app(argc, argv); + QCoreApplication::setOrganizationName(QStringLiteral("KDE")); + QCoreApplication::setApplicationName(QStringLiteral("%{APPNAME}")); + + KAboutData aboutData( + // The program name used internally. + QStringLiteral("%{APPNAME}"), + // 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", "Author Role"), + QStringLiteral("%{EMAIL}"), + QStringLiteral("https://yourwebsite.com")); + KAboutData::setApplicationData(aboutData); + + QQmlApplicationEngine engine; + + auto config = %{APPNAME}Config::self(); + + qmlRegisterSingletonInstance("org.kde.%{APPNAME}", 1, 0, "Config", config); + + AboutType about; + qmlRegisterSingletonInstance("org.kde.%{APPNAME}", 1, 0, "AboutType", &about); + + App application; + qmlRegisterSingletonInstance("org.kde.%{APPNAME}", 1, 0, "App", &application); + + engine.rootContext()->setContextObject(new KLocalizedContext(&engine)); + engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); + + if (engine.rootObjects().isEmpty()) { + return -1; + } + + return app.exec(); +} diff --git a/templates/kirigami/src/resources.qrc b/templates/kirigami/src/resources.qrc new file mode 100644 index 0000000..9c5b3b4 --- /dev/null +++ b/templates/kirigami/src/resources.qrc @@ -0,0 +1,10 @@ + + + + contents/ui/main.qml + contents/ui/About.qml + + diff --git a/tests/BasicListItemTest.qml b/tests/BasicListItemTest.qml new file mode 100644 index 0000000..0ff8ba8 --- /dev/null +++ b/tests/BasicListItemTest.qml @@ -0,0 +1,222 @@ +/* + * SPDX-FileCopyrightText: 2021 Nate Graham + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 as QQC2 + +import org.kde.kirigami 2.20 as Kirigami + +Kirigami.ApplicationWindow { + GridLayout { + anchors.fill: parent + anchors.margins: Kirigami.Units.gridUnit + + rows: 3 + rowSpacing: Kirigami.Units.gridUnit + columns: 3 + columnSpacing: Kirigami.Units.gridUnit + + // Icon + Label + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + Kirigami.Heading { + text: "Icon + Label" + level: 3 + Layout.fillWidth: true + wrapMode: Text.Wrap + } + QQC2.ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + Component.onCompleted: { + background.visible = true; + } + ListView { + model: 3 + delegate: Kirigami.BasicListItem { + icon: "edit-bomb" + text: "Boom!" + } + } + } + } + + // Label + space reserved for icon + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + Kirigami.Heading { + text: "Icon + Label + space reserved for icon" + level: 3 + Layout.fillWidth: true + wrapMode: Text.Wrap + } + QQC2.ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + Component.onCompleted: { + background.visible = true; + } + ListView { + model: 3 + delegate: Kirigami.BasicListItem { + text: "Boom!" + reserveSpaceForIcon: true + } + } + } + } + + // Icon + Label + leading and trailing items + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + Kirigami.Heading { + text: "Icon + Label + leading and trailing items" + level: 3 + Layout.fillWidth: true + wrapMode: Text.Wrap + } + QQC2.ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + Component.onCompleted: { + background.visible = true; + } + ListView { + model: 3 + delegate: Kirigami.BasicListItem { + leading: Rectangle { + radius: width * 0.5 + width: Kirigami.Units.largeSpacing + height: Kirigami.Units.largeSpacing + Kirigami.Theme.colorSet: Kirigami.Theme.View + color: Kirigami.Theme.neutralTextColor + } + leadingFillVertically: false + + icon: "edit-bomb" + text: "Boom!" + + trailing: QQC2.Button { + text: "Defuse the bomb!" + icon.name: "edit-delete" + } + } + } + } + } + + // Icon + Label + subtitle + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + Kirigami.Heading { + text: "Icon + Label + subtitle" + level: 3 + Layout.fillWidth: true + wrapMode: Text.Wrap + } + QQC2.ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + Component.onCompleted: { + background.visible = true; + } + ListView { + model: 3 + delegate: Kirigami.BasicListItem { + icon: "edit-bomb" + text: "Boom!" + subtitle: "smaller boom" + } + } + } + } + + // Icon + Label + space reserved for subtitle + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + Kirigami.Heading { + text: "Icon + Label + space reserved for subtitle" + level: 3 + Layout.fillWidth: true + wrapMode: Text.Wrap + } + QQC2.ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + Component.onCompleted: { + background.visible = true; + } + ListView { + model: 3 + delegate: Kirigami.BasicListItem { + icon: "edit-bomb" + text: "Boom!" + reserveSpaceForSubtitle: true + } + } + } + } + + // Icon + Label + subtitle + leading and trailing items + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + Kirigami.Heading { + text: "Icon + Label + subtitle + leading and trailing items" + level: 3 + Layout.fillWidth: true + wrapMode: Text.Wrap + } + QQC2.ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + Component.onCompleted: { + background.visible = true; + } + ListView { + model: 3 + delegate: Kirigami.BasicListItem { + leading: Rectangle { + radius: width * 0.5 + width: Kirigami.Units.largeSpacing + height: Kirigami.Units.largeSpacing + Kirigami.Theme.colorSet: Kirigami.Theme.View + color: Kirigami.Theme.neutralTextColor + } + leadingFillVertically: false + + icon: "edit-bomb" + text: "Boom!" + subtitle: "smaller boom" + + trailing: QQC2.Button { + text: "Defuse the bomb!" + icon.name: "edit-delete" + } + } + } + } + } + } +} diff --git a/tests/KeyboardListTest.qml b/tests/KeyboardListTest.qml new file mode 100644 index 0000000..87816c7 --- /dev/null +++ b/tests/KeyboardListTest.qml @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import org.kde.kirigami 2.20 as Kirigami + +Kirigami.ApplicationWindow { + id: main + + pageStack.initialPage: Kirigami.ScrollablePage { + ListView { + model: 10 + delegate: Rectangle { + width: 100 + height: 30 + color: ListView.isCurrentItem ? "red" : "white" + } + } + } +} diff --git a/tests/KeyboardTest.qml b/tests/KeyboardTest.qml new file mode 100644 index 0000000..ed65b00 --- /dev/null +++ b/tests/KeyboardTest.qml @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import org.kde.kirigami 2.20 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/NavigationTabBarTest.qml b/tests/NavigationTabBarTest.qml new file mode 100644 index 0000000..87a93a7 --- /dev/null +++ b/tests/NavigationTabBarTest.qml @@ -0,0 +1,93 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 + +import org.kde.kirigami 2.20 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 + 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 + } + ] + } +} diff --git a/tests/OverlayFocusTest.qml b/tests/OverlayFocusTest.qml new file mode 100644 index 0000000..7617ddb --- /dev/null +++ b/tests/OverlayFocusTest.qml @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2021 Ismael Asensio + * SPDX-FileCopyrightText: 2021 David Edmundson + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 as QQC2 + +import org.kde.kirigami 2.20 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: Kirigami.BasicListItem { + label: "Item " + modelData + } + } + } +} diff --git a/tests/OverlayTest.qml b/tests/OverlayTest.qml new file mode 100644 index 0000000..073ab80 --- /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 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 as QQC2 + +import org.kde.kirigami 2.20 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..f829074 --- /dev/null +++ b/tests/ShadowedImageTest.qml @@ -0,0 +1,67 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Dialogs 1.3 as Dialogs + +import org.kde.kirigami 2.20 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..c62ae36 --- /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 2.15 +import QtQuick.Controls 2.15 + +import org.kde.kirigami 2.20 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..c002b61 --- /dev/null +++ b/tests/actionsMenu.qml @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2016 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.20 as Kirigami + +Kirigami.ApplicationWindow { + id: main + + header: Kirigami.ToolBarApplicationHeader {} + + 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" + actions { + main: Kirigami.Action { icon.name: "kate"; text: "BonDia" } + left : Kirigami.Action { icon.name: "kate"; text: "BonDia" } + right: Kirigami.Action { icon.name: "kate"; text: "BonDia" } + } + + QQC2.ActionGroup { + id: group + } + + contextualActions: [ + 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..d6b84c4 --- /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 2.15 +import QtQuick.Controls 2.15 + +import org.kde.kirigami 2.20 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/swipeListItemTest.qml b/tests/swipeListItemTest.qml new file mode 100644 index 0000000..0af244b --- /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 2.15 +import QtQuick.Controls 2.15 + +import org.kde.kirigami 2.20 as Kirigami + +Kirigami.ApplicationWindow { + id: main + + pageStack.initialPage: Kirigami.ScrollablePage { + ListView { + model: 25 + delegate: Kirigami.SwipeListItem { + supportsMouseEvents: false + actions: [ + Kirigami.Action { + iconName: "go-up" + } + ] + 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..f4bad04 --- /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 2.15 +import QtQuick.Templates 2.15 as T +import QtQuick.Controls 2.15 as QQC2 + +import org.kde.kirigami 2.20 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..4e4f65a --- /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 2.15 +import QtQuick.Controls 2.15 as QQC2 + +import org.kde.kirigami 2.20 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..5d66365 --- /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 2.15 +import QtQml 2.15 +import QtQuick.Templates 2.15 as T +import QtQuick.Controls 2.15 as QQC2 + +import org.kde.kirigami 2.20 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..fef326d --- /dev/null +++ b/tests/wheelhandler/WheelHandlerScrollViewTextAreaTest.qml @@ -0,0 +1,25 @@ +/* SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + */ + +import QtQuick 2.15 +import QtQml 2.15 +import QtQuick.Templates 2.15 as T +import QtQuick.Controls 2.15 as QQC2 + +import org.kde.kirigami 2.20 as Kirigami + +QQC2.ApplicationWindow { + id: root + width: 600 + height: 600 + visible: true + + ScrollView { + id: scrollView + anchors.fill: parent + QQC2.TextArea { + 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 + + + + + + + + +